@dshtrading/connector-yahoo 0.1.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 +75 -0
- package/README.md +25 -0
- package/lib/dataplane.d.ts +6 -0
- package/lib/dataplane.js +22 -0
- package/lib/index.d.ts +83 -0
- package/lib/index.js +235 -0
- package/lib/rest.d.ts +77 -0
- package/lib/rest.js +175 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Copyright (c) 2026 zhu1090093659 (dsh-trading)
|
|
2
|
+
|
|
3
|
+
# PolyForm Noncommercial License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncommercial Purposes
|
|
34
|
+
|
|
35
|
+
Any noncommercial purpose is a permitted purpose.
|
|
36
|
+
|
|
37
|
+
## Personal Uses
|
|
38
|
+
|
|
39
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
40
|
+
|
|
41
|
+
## Noncommercial Organizations
|
|
42
|
+
|
|
43
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
44
|
+
|
|
45
|
+
## Fair Use
|
|
46
|
+
|
|
47
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
48
|
+
|
|
49
|
+
## No Other Rights
|
|
50
|
+
|
|
51
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
52
|
+
|
|
53
|
+
## Patent Defense
|
|
54
|
+
|
|
55
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
56
|
+
|
|
57
|
+
## Violations
|
|
58
|
+
|
|
59
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
60
|
+
|
|
61
|
+
## No Liability
|
|
62
|
+
|
|
63
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
64
|
+
|
|
65
|
+
## Definitions
|
|
66
|
+
|
|
67
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
68
|
+
|
|
69
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
72
|
+
|
|
73
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
74
|
+
|
|
75
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @dshtrading/connector-yahoo
|
|
2
|
+
|
|
3
|
+
dsh-trading us 切片市场连接器:经 **Yahoo Finance v8 chart API**(`query1.finance.yahoo.com/v8/finance/chart/...`)实现 `@dshtrading/api` 的 `MarketDataService` 契约(ctx 键 `tradingUsMarketData`),并提供 `us_get_ticker` / `us_get_klines` / `us_place_order` 三工具(下单三段闸门与 connector-stooq/binance 同构)。
|
|
4
|
+
|
|
5
|
+
## 数据源状态:已实证(2026-08-29)
|
|
6
|
+
|
|
7
|
+
本出口(开发机)实测可用:`interval=1d&range=5d` 返回完整 `meta` + `timestamp[]` + `indicators.quote[0]` 序列;需 `User-Agent: Mozilla/5.0` 头。ticker + klines 真实请求证据与交叉一致性分析见 `spikes/impl-us-yahoo/`(probe-output.txt / ticker-AAPL.json / klines-AAPL-1d.json / klines-AAPL-60m.json / EVIDENCE.md)。
|
|
8
|
+
|
|
9
|
+
已知局限(工具描述同步向模型说明):
|
|
10
|
+
|
|
11
|
+
- **非官方 API**:无 key、无 SLA、无稳定性承诺;主机为 query1/query2 双活备用。
|
|
12
|
+
- **日线汇总滞后**:最新已收盘交易日的日 K 可能延后补齐(实证:周五收盘后周六早晨日线序列仍缺周五,但 60m 序列完整且收盘价与 `meta.regularMarketPrice` 一致到 float32 精度)。`getTicker` 的价格/时间取 meta(权威实时面),volume 取最新日 K 量、可能滞后一个交易日。
|
|
13
|
+
- 1m 线历史上限约 7 天(Yahoo 端约束)。
|
|
14
|
+
|
|
15
|
+
## 合规记录(README 铁律 #5)
|
|
16
|
+
|
|
17
|
+
Yahoo Finance 的 v8 chart API 为**非官方接口**:个人使用属灰色但被普遍使用的边界。本仓按现状如实记录:
|
|
18
|
+
|
|
19
|
+
- 无凭证、无 key;仅拉取公开行情,**本仓不缓存、不批量抓取、不再次分发**任何行情数据;
|
|
20
|
+
- 使用边界以 [Yahoo Terms of Use](https://legal.yahoo.com/terms-of-use/) 与其数据提供方条款为准;
|
|
21
|
+
- 若 Yahoo 收紧该接口(风控/鉴权),本连接器会如实报 `TRADING_*` 错误,不做伪装或挑战求解。
|
|
22
|
+
|
|
23
|
+
## 前任数据源
|
|
24
|
+
|
|
25
|
+
us 切片原用 connector-stooq;Stooq 自本出口被反爬拒止(挑战页 + Access denied,证据 `spikes/impl-us/REPORT.md`),无成功实证,于任务 G 切换至 Yahoo。connector-stooq 代码保留(其他出口可能可用),状态见其 README。
|
package/lib/dataplane.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TRADING_US_MARKET_DATA_KEY, YahooMarketDataService } from "./index.js";
|
|
2
|
+
//#region src/dataplane.ts
|
|
3
|
+
const inject = [];
|
|
4
|
+
/** 解析注册表服务;老部署(base/router 未升级)返回 undefined → 调用方回退旧的直接 provide 路径。 */
|
|
5
|
+
function resolveMarketDataRegistry(ctx) {
|
|
6
|
+
const candidate = ctx.get?.("tradingMarketDataRegistry", false);
|
|
7
|
+
return candidate !== void 0 ? candidate : void 0;
|
|
8
|
+
}
|
|
9
|
+
/** 本连接器的路由 provider slug(路由层词汇,docs/exchange-routing.md §2.2)。 */
|
|
10
|
+
const ROUTER_PROVIDER = "yahoo";
|
|
11
|
+
function apply(ctx) {
|
|
12
|
+
const registry = resolveMarketDataRegistry(ctx);
|
|
13
|
+
if (registry === void 0) {
|
|
14
|
+
new YahooMarketDataService(ctx);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const inner = ctx.isolate(TRADING_US_MARKET_DATA_KEY);
|
|
18
|
+
const service = new YahooMarketDataService(inner);
|
|
19
|
+
ctx.effect(() => registry.register("us", ROUTER_PROVIDER, service));
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { apply, inject };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { INTERVAL_VOCABULARY, TradingServiceError, YahooChartEnvelope, YahooChartResult, YahooRestClient, YahooRestOptions, isSupportedInterval, normalizeYahooSymbol, parseChartBars } from "./rest.js";
|
|
2
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
4
|
+
import { Disposable, Interval, Kline, MarketDataService, Ticker } from "@dshtrading/api";
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Cordis 插件名 = patch 行 id(TEMPLATES §8):`dsh-trading-us-*` 市场命名空间,
|
|
8
|
+
* 全仓唯一(insert-only 铁律 #1)。
|
|
9
|
+
*/
|
|
10
|
+
declare const name = "dsh-trading-us-connector-yahoo";
|
|
11
|
+
interface Config {
|
|
12
|
+
/** 交易安全闸门(铁律 #3):true 时下单类工具强制 dry-run。 */
|
|
13
|
+
dryRun: boolean;
|
|
14
|
+
/** 实盘总闸门:默认 false;false 时无论 dryRun 与否都拒绝实盘下单 [S4]。 */
|
|
15
|
+
liveTrading: boolean;
|
|
16
|
+
}
|
|
17
|
+
declare const Config: Schema<Config>;
|
|
18
|
+
/** 需要宿主提供的 Cordis 服务。 */
|
|
19
|
+
declare const inject: string[];
|
|
20
|
+
/** ctx 服务键:与 @dshtrading/api 的 Context 模块增强一致(市场命名空间)。 */
|
|
21
|
+
declare const TRADING_US_MARKET_DATA_KEY = "tradingUsMarketData";
|
|
22
|
+
interface SubscribeTickerOptions {
|
|
23
|
+
/** 轮询间隔(ms)。subscribeTicker 以 meta 快照轮询实现(Yahoo 无官方推送面)。 */
|
|
24
|
+
readonly intervalMs?: number;
|
|
25
|
+
}
|
|
26
|
+
declare class YahooMarketDataService extends Service implements MarketDataService {
|
|
27
|
+
private readonly client;
|
|
28
|
+
constructor(ctx: Context, options?: YahooRestOptions, name?: string);
|
|
29
|
+
getTicker(symbol: string): Promise<Ticker>;
|
|
30
|
+
getKlines(symbol: string, interval: Interval, limit?: number): Promise<Kline[]>;
|
|
31
|
+
subscribeTicker(symbol: string, cb: (ticker: Ticker) => void, options?: SubscribeTickerOptions): Disposable;
|
|
32
|
+
}
|
|
33
|
+
/** us_place_order 参数契约(美股词汇,dryRun 缺省 true)。 */
|
|
34
|
+
interface PlaceOrderArgs {
|
|
35
|
+
/** 股票代码,如 `AAPL` 或 `brk-b`(执行前归一化为 Yahoo 大写形态)。 */
|
|
36
|
+
readonly symbol: string;
|
|
37
|
+
readonly side: 'BUY' | 'SELL';
|
|
38
|
+
readonly type: 'MARKET' | 'LIMIT';
|
|
39
|
+
/** 股数(整股),必须 > 0。 */
|
|
40
|
+
readonly quantity: number;
|
|
41
|
+
/** LIMIT 单必填(schema 无法条件必填,execute 内校验),必须 > 0。 */
|
|
42
|
+
readonly price?: number;
|
|
43
|
+
/** 缺省视为 true:仅模拟。显式 false 即实盘意图,进入闸门 ①/③。 */
|
|
44
|
+
readonly dryRun?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 闸门判定结果(三条路径与裁决顺序与 crypto/stooq 同构,见 connector-binance evaluateOrderGate)。
|
|
48
|
+
*/
|
|
49
|
+
type OrderGateVerdict = {
|
|
50
|
+
action: 'reject';
|
|
51
|
+
code: 'TRADING_LIVE_TRADING_DISABLED';
|
|
52
|
+
message: string;
|
|
53
|
+
} | {
|
|
54
|
+
action: 'simulate';
|
|
55
|
+
} | {
|
|
56
|
+
action: 'live';
|
|
57
|
+
};
|
|
58
|
+
declare function evaluateOrderGate(config: Config, args: PlaceOrderArgs): OrderGateVerdict;
|
|
59
|
+
/** DRY-RUN 回执:模拟成交 + Yahoo 最新 regular-market 参照(参照取不到不阻断模拟本身)。 */
|
|
60
|
+
interface DryRunReference {
|
|
61
|
+
source: 'yahoo-regular-market';
|
|
62
|
+
price?: number;
|
|
63
|
+
volume?: number;
|
|
64
|
+
timestamp?: number;
|
|
65
|
+
unavailable?: string;
|
|
66
|
+
}
|
|
67
|
+
declare function buildDryRunReceipt(args: PlaceOrderArgs, marketData: Pick<MarketDataService, 'getTicker'>): Promise<string>;
|
|
68
|
+
interface PlaceOrderToolDeps {
|
|
69
|
+
/** 行情服务(dry-run 回执的市价参照),按接口取用,不直连 HTTP。 */
|
|
70
|
+
readonly marketData: Pick<MarketDataService, 'getTicker'>;
|
|
71
|
+
/** 插件配置(dryRun 强制模拟 / liveTrading 总闸门)。 */
|
|
72
|
+
readonly config: Config;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* us_place_order 工具工厂(独立导出便于单测三条闸门路径)。
|
|
76
|
+
*
|
|
77
|
+
* 审批不在这里做:dryRun!==true 的调用由 @dshtrading/base 的 gate 插件在
|
|
78
|
+
* `tools/pre-execute` waterfall 统一 ask(S4:headless 下 ask=deny,fail-closed)。
|
|
79
|
+
*/
|
|
80
|
+
declare function createPlaceOrderTool(deps: PlaceOrderToolDeps): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
|
81
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
82
|
+
//#endregion
|
|
83
|
+
export { Config, DryRunReference, INTERVAL_VOCABULARY, OrderGateVerdict, PlaceOrderArgs, PlaceOrderToolDeps, SubscribeTickerOptions, TRADING_US_MARKET_DATA_KEY, TradingServiceError, YahooChartEnvelope, YahooChartResult, YahooMarketDataService, YahooRestClient, YahooRestOptions, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, isSupportedInterval, name, normalizeYahooSymbol, parseChartBars };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { INTERVAL_VOCABULARY, TradingServiceError, YahooRestClient, isSupportedInterval, normalizeYahooSymbol, parseChartBars } from "./rest.js";
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
/**
|
|
7
|
+
* Cordis 插件名 = patch 行 id(TEMPLATES §8):`dsh-trading-us-*` 市场命名空间,
|
|
8
|
+
* 全仓唯一(insert-only 铁律 #1)。
|
|
9
|
+
*/
|
|
10
|
+
const name = "dsh-trading-us-connector-yahoo";
|
|
11
|
+
const Config = Schema.object({
|
|
12
|
+
dryRun: Schema.boolean().default(true),
|
|
13
|
+
liveTrading: Schema.boolean().default(false)
|
|
14
|
+
});
|
|
15
|
+
/** 需要宿主提供的 Cordis 服务。 */
|
|
16
|
+
const inject = ["tools"];
|
|
17
|
+
/** ctx 服务键:与 @dshtrading/api 的 Context 模块增强一致(市场命名空间)。 */
|
|
18
|
+
const TRADING_US_MARKET_DATA_KEY = "tradingUsMarketData";
|
|
19
|
+
const SUBSCRIBE_MIN_MS = 250;
|
|
20
|
+
const SUBSCRIBE_DEFAULT_MS = 5e3;
|
|
21
|
+
var YahooMarketDataService = class extends Service {
|
|
22
|
+
client;
|
|
23
|
+
constructor(ctx, options = {}, name = TRADING_US_MARKET_DATA_KEY) {
|
|
24
|
+
super(ctx, name);
|
|
25
|
+
this.client = new YahooRestClient(options);
|
|
26
|
+
}
|
|
27
|
+
getTicker(symbol) {
|
|
28
|
+
return this.client.getTicker(symbol);
|
|
29
|
+
}
|
|
30
|
+
getKlines(symbol, interval, limit) {
|
|
31
|
+
return this.client.getKlines(symbol, interval).then((all) => typeof limit === "number" && Number.isInteger(limit) && limit > 0 ? all.slice(-limit) : all);
|
|
32
|
+
}
|
|
33
|
+
subscribeTicker(symbol, cb, options) {
|
|
34
|
+
const ms = Math.max(options?.intervalMs ?? SUBSCRIBE_DEFAULT_MS, SUBSCRIBE_MIN_MS);
|
|
35
|
+
const tick = () => {
|
|
36
|
+
this.client.getTicker(symbol).then(cb, () => {});
|
|
37
|
+
};
|
|
38
|
+
tick();
|
|
39
|
+
const timer = setInterval(tick, ms);
|
|
40
|
+
return { dispose: () => clearInterval(timer) };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function evaluateOrderGate(config, args) {
|
|
44
|
+
const requestedDryRun = args.dryRun ?? true;
|
|
45
|
+
if (!requestedDryRun && !config.liveTrading) return {
|
|
46
|
+
action: "reject",
|
|
47
|
+
code: "TRADING_LIVE_TRADING_DISABLED",
|
|
48
|
+
message: `us_place_order rejected: the call requests real execution (dryRun=${String(args.dryRun)}) but live trading is disabled (liveTrading=false). Ask the user to enable liveTrading explicitly after confirmation, or keep dryRun=true for a simulated fill.`
|
|
49
|
+
};
|
|
50
|
+
if (requestedDryRun || config.dryRun) return { action: "simulate" };
|
|
51
|
+
return { action: "live" };
|
|
52
|
+
}
|
|
53
|
+
/** 参数校验(模型调用问题抛普通 Error,与 crypto 先例一致;服务故障才用错误词汇)。 */
|
|
54
|
+
function validatePlaceOrderArgs(args) {
|
|
55
|
+
if (typeof args.symbol !== "string" || !args.symbol.trim()) throw new Error("us_place_order: invalid symbol — expected a US ticker like AAPL or BRK-B");
|
|
56
|
+
try {
|
|
57
|
+
normalizeYahooSymbol(args.symbol);
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
throw new Error(`us_place_order: invalid symbol ${JSON.stringify(args.symbol)} — expected a US ticker like AAPL`, { cause });
|
|
60
|
+
}
|
|
61
|
+
if (args.side !== "BUY" && args.side !== "SELL") throw new Error(`us_place_order: invalid side ${JSON.stringify(args.side)} — expected BUY or SELL`);
|
|
62
|
+
if (args.type !== "MARKET" && args.type !== "LIMIT") throw new Error(`us_place_order: invalid type ${JSON.stringify(args.type)} — expected MARKET or LIMIT`);
|
|
63
|
+
if (typeof args.quantity !== "number" || !Number.isFinite(args.quantity) || args.quantity <= 0) throw new Error(`us_place_order: invalid quantity ${JSON.stringify(args.quantity)} — expected a positive number`);
|
|
64
|
+
if (args.type === "LIMIT" && (typeof args.price !== "number" || !Number.isFinite(args.price) || args.price <= 0)) throw new Error("us_place_order: LIMIT orders require a positive price");
|
|
65
|
+
}
|
|
66
|
+
function normalizePlaceOrderArgs(raw) {
|
|
67
|
+
const args = raw ?? {};
|
|
68
|
+
const symbol = typeof args.symbol === "string" ? args.symbol.trim() : void 0;
|
|
69
|
+
return {
|
|
70
|
+
...args,
|
|
71
|
+
symbol
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function buildDryRunReceipt(args, marketData) {
|
|
75
|
+
const symbol = normalizeYahooSymbol(args.symbol);
|
|
76
|
+
let reference;
|
|
77
|
+
try {
|
|
78
|
+
const ticker = await marketData.getTicker(symbol);
|
|
79
|
+
reference = {
|
|
80
|
+
source: "yahoo-regular-market",
|
|
81
|
+
price: ticker.price,
|
|
82
|
+
volume: ticker.volume,
|
|
83
|
+
timestamp: ticker.timestamp
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
reference = {
|
|
87
|
+
source: "yahoo-regular-market",
|
|
88
|
+
unavailable: error instanceof Error ? error.message : String(error)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return JSON.stringify({
|
|
92
|
+
status: "filled",
|
|
93
|
+
dryRun: true,
|
|
94
|
+
note: "DRY-RUN — simulated fill; no order was sent to any broker. Yahoo Finance is a data source only (no trading API); the reference price is the latest regular-market price, which may not reflect pre/post-market moves.",
|
|
95
|
+
id: `dry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
96
|
+
symbol,
|
|
97
|
+
side: args.side.toLowerCase(),
|
|
98
|
+
type: args.type.toLowerCase(),
|
|
99
|
+
quantity: args.quantity,
|
|
100
|
+
...args.type === "LIMIT" ? { price: args.price } : {},
|
|
101
|
+
reference,
|
|
102
|
+
timestamp: Date.now()
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* us_place_order 工具工厂(独立导出便于单测三条闸门路径)。
|
|
107
|
+
*
|
|
108
|
+
* 审批不在这里做:dryRun!==true 的调用由 @dshtrading/base 的 gate 插件在
|
|
109
|
+
* `tools/pre-execute` waterfall 统一 ask(S4:headless 下 ask=deny,fail-closed)。
|
|
110
|
+
*/
|
|
111
|
+
function createPlaceOrderTool(deps) {
|
|
112
|
+
return defineTool({
|
|
113
|
+
name: "us_place_order",
|
|
114
|
+
description: "Place a US stock order, or simulate one. dryRun defaults to true and returns a DRY-RUN simulated fill receipt referencing the latest Yahoo Finance regular-market price. Real execution (dryRun=false) requires the plugin liveTrading switch plus user approval, and is not implemented yet in this slice (Yahoo has no trading API).",
|
|
115
|
+
parameters: {
|
|
116
|
+
symbol: {
|
|
117
|
+
type: "string",
|
|
118
|
+
required: true,
|
|
119
|
+
description: "US stock ticker, e.g. AAPL or BRK-B"
|
|
120
|
+
},
|
|
121
|
+
side: {
|
|
122
|
+
type: "string",
|
|
123
|
+
enum: ["BUY", "SELL"],
|
|
124
|
+
required: true,
|
|
125
|
+
description: "Order side"
|
|
126
|
+
},
|
|
127
|
+
type: {
|
|
128
|
+
type: "string",
|
|
129
|
+
enum: ["MARKET", "LIMIT"],
|
|
130
|
+
required: true,
|
|
131
|
+
description: "Order type"
|
|
132
|
+
},
|
|
133
|
+
quantity: {
|
|
134
|
+
type: "number",
|
|
135
|
+
required: true,
|
|
136
|
+
description: "Share quantity (whole shares), must be > 0"
|
|
137
|
+
},
|
|
138
|
+
price: {
|
|
139
|
+
type: "number",
|
|
140
|
+
description: "Limit price; required when type=LIMIT"
|
|
141
|
+
},
|
|
142
|
+
dryRun: {
|
|
143
|
+
type: "boolean",
|
|
144
|
+
description: "true (default) = simulate only and return a DRY-RUN receipt; false = request real execution (gated by liveTrading and user approval)",
|
|
145
|
+
default: true
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
output: {
|
|
149
|
+
schema: { type: "string" },
|
|
150
|
+
render: (_args, value) => [{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: value
|
|
153
|
+
}]
|
|
154
|
+
},
|
|
155
|
+
async execute(raw) {
|
|
156
|
+
const args = normalizePlaceOrderArgs(raw);
|
|
157
|
+
validatePlaceOrderArgs(args);
|
|
158
|
+
const verdict = evaluateOrderGate(deps.config, args);
|
|
159
|
+
if (verdict.action === "reject") return JSON.stringify({
|
|
160
|
+
status: "rejected",
|
|
161
|
+
code: verdict.code,
|
|
162
|
+
message: verdict.message
|
|
163
|
+
});
|
|
164
|
+
if (verdict.action === "simulate") return buildDryRunReceipt(args, deps.marketData);
|
|
165
|
+
throw new TradingServiceError("TRADING_NOT_IMPLEMENTED", "us_place_order: live order execution is not implemented in this slice — Yahoo Finance provides market data only; a broker API (e.g. Alpaca/IBKR) is a follow-up. Keep dryRun=true for simulated fills.");
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function apply(ctx, config) {
|
|
170
|
+
new YahooMarketDataService(ctx);
|
|
171
|
+
ctx.inject(["tradingUsMarketData"], (ctx) => {
|
|
172
|
+
const marketData = ctx.tradingUsMarketData;
|
|
173
|
+
ctx.tools.register(defineTool({
|
|
174
|
+
name: "us_get_ticker",
|
|
175
|
+
description: "Get the latest market snapshot for a US stock via the Yahoo Finance v8 chart API (unofficial; no key). Returns the latest regular-market price/time (meta.regularMarketPrice/Time), the official previous close as prevClose (change anchor) with changePercent, and the session volume (meta.regularMarketVolume — official figure, unaffected by Yahoo's daily-bar consolidation lag). Personal use per Yahoo terms of use.",
|
|
176
|
+
parameters: { symbol: {
|
|
177
|
+
type: "string",
|
|
178
|
+
required: true,
|
|
179
|
+
description: "US stock ticker, e.g. AAPL or BRK-B (normalized to upper-case internally)"
|
|
180
|
+
} },
|
|
181
|
+
output: {
|
|
182
|
+
schema: { type: "string" },
|
|
183
|
+
render: (_args, value) => [{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: value
|
|
186
|
+
}]
|
|
187
|
+
},
|
|
188
|
+
async execute(args) {
|
|
189
|
+
const ticker = await marketData.getTicker(args.symbol);
|
|
190
|
+
return JSON.stringify(ticker);
|
|
191
|
+
}
|
|
192
|
+
}));
|
|
193
|
+
ctx.tools.register(defineTool({
|
|
194
|
+
name: "us_get_klines",
|
|
195
|
+
description: "Get recent OHLCV candles for a US stock via the Yahoo Finance v8 chart API (unofficial; no key). Supported intervals: 1m/5m/15m/30m (intraday history is limited by Yahoo: 1m caps at ~7 days), 1h, 1d, 1w, 1M. Personal use per Yahoo terms of use.",
|
|
196
|
+
parameters: {
|
|
197
|
+
symbol: {
|
|
198
|
+
type: "string",
|
|
199
|
+
required: true,
|
|
200
|
+
description: "US stock ticker, e.g. AAPL or BRK-B (normalized to upper-case internally)"
|
|
201
|
+
},
|
|
202
|
+
interval: {
|
|
203
|
+
type: "string",
|
|
204
|
+
enum: INTERVAL_VOCABULARY,
|
|
205
|
+
description: "Kline interval (Yahoo-supported subset)",
|
|
206
|
+
default: "1d"
|
|
207
|
+
},
|
|
208
|
+
limit: {
|
|
209
|
+
type: "integer",
|
|
210
|
+
description: "Number of most recent candles to return (1-1000; the client fetches a fixed window and the tool trims)",
|
|
211
|
+
default: 100
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
output: {
|
|
215
|
+
schema: { type: "string" },
|
|
216
|
+
render: (_args, value) => [{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: value
|
|
219
|
+
}]
|
|
220
|
+
},
|
|
221
|
+
async execute(args) {
|
|
222
|
+
const interval = args.interval ?? "1d";
|
|
223
|
+
const limit = args.limit ?? 100;
|
|
224
|
+
const klines = await marketData.getKlines(args.symbol, interval, limit);
|
|
225
|
+
return JSON.stringify(klines);
|
|
226
|
+
}
|
|
227
|
+
}));
|
|
228
|
+
ctx.tools.register(createPlaceOrderTool({
|
|
229
|
+
marketData,
|
|
230
|
+
config
|
|
231
|
+
}));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
export { Config, INTERVAL_VOCABULARY, TRADING_US_MARKET_DATA_KEY, TradingServiceError, YahooMarketDataService, YahooRestClient, apply, buildDryRunReceipt, createPlaceOrderTool, evaluateOrderGate, inject, isSupportedInterval, name, normalizeYahooSymbol, parseChartBars };
|
package/lib/rest.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Interval, Kline, Ticker, TradingErrorCode } from "@dshtrading/api";
|
|
2
|
+
//#region src/rest.d.ts
|
|
3
|
+
/** api 包 TradingError 契约的运行时 Error 实现。 */
|
|
4
|
+
declare class TradingServiceError extends Error {
|
|
5
|
+
readonly code: TradingErrorCode;
|
|
6
|
+
constructor(code: TradingErrorCode, message: string, cause?: unknown);
|
|
7
|
+
}
|
|
8
|
+
/** 规范化并校验符号:接受 `AAPL` / `aapl` / `brk-b`,统一为大写;不补任何后缀。 */
|
|
9
|
+
declare function normalizeYahooSymbol(symbol: string): string;
|
|
10
|
+
/** 工具 parameters enum 用:受支持 interval 词汇(与 connector-stooq 同一子集口径)。 */
|
|
11
|
+
declare const INTERVAL_VOCABULARY: readonly string[];
|
|
12
|
+
declare function isSupportedInterval(value: Interval): boolean;
|
|
13
|
+
/** v8 chart 响应的最小类型面(只取本客户端消费的字段)。 */
|
|
14
|
+
interface YahooChartResult {
|
|
15
|
+
meta: {
|
|
16
|
+
currency?: string;
|
|
17
|
+
regularMarketPrice?: number;
|
|
18
|
+
regularMarketTime?: number;
|
|
19
|
+
regularMarketDayHigh?: number;
|
|
20
|
+
regularMarketDayLow?: number;
|
|
21
|
+
regularMarketVolume?: number;
|
|
22
|
+
regularMarketChangePercent?: number;
|
|
23
|
+
/** range 窗口首根前一收盘的锚点(range=1d 时即官方昨收,见 getTicker 注释)。 */
|
|
24
|
+
chartPreviousClose?: number;
|
|
25
|
+
previousClose?: number;
|
|
26
|
+
exchangeTimezoneName?: string;
|
|
27
|
+
};
|
|
28
|
+
timestamp: number[];
|
|
29
|
+
indicators: {
|
|
30
|
+
quote: Array<{
|
|
31
|
+
open?: Array<number | null>;
|
|
32
|
+
high?: Array<number | null>;
|
|
33
|
+
low?: Array<number | null>;
|
|
34
|
+
close?: Array<number | null>;
|
|
35
|
+
volume?: Array<number | null>;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
interface YahooChartEnvelope {
|
|
40
|
+
chart: {
|
|
41
|
+
error?: {
|
|
42
|
+
code?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
} | null;
|
|
45
|
+
result?: YahooChartResult[] | null;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** 把 timestamp[] + quote 序列对齐成 Kline[](旧→新;null 行如实丢弃)。 */
|
|
49
|
+
declare function parseChartBars(result: YahooChartResult, interval: Interval): Kline[];
|
|
50
|
+
interface YahooRestOptions {
|
|
51
|
+
/** 覆盖 API base(测试/反代用),末尾不带斜杠。query2 是同一服务的备用主机。 */
|
|
52
|
+
readonly baseUrl?: string;
|
|
53
|
+
/** 单请求超时(ms),默认 10s。 */
|
|
54
|
+
readonly timeoutMs?: number;
|
|
55
|
+
/** 注入 fetch 实现;缺省用全局 fetch(Node 22+ 内置)。 */
|
|
56
|
+
readonly fetchImpl?: typeof fetch;
|
|
57
|
+
/** 覆盖 User-Agent(默认 Mozilla/5.0 裸 UA,实测够用)。 */
|
|
58
|
+
readonly userAgent?: string;
|
|
59
|
+
}
|
|
60
|
+
declare class YahooRestClient {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(options?: YahooRestOptions);
|
|
63
|
+
/** 拉取并解析 K 线(旧→新全窗口,由服务层按 limit 截尾)。 */
|
|
64
|
+
getKlines(symbol: string, interval: Interval): Promise<Kline[]>;
|
|
65
|
+
/**
|
|
66
|
+
* 最新行情快照:单请求 interval=1d&range=1d。
|
|
67
|
+
* price/time/volume/prevClose 全取 meta(权威实时面):昨收锚点取 meta.chartPreviousClose
|
|
68
|
+
* ——range=1d 窗口恰只含最新一个会话,其「窗口首根前一收盘」即官方昨收,与富途等终端
|
|
69
|
+
* 同语义(2026-09-01 AAPL 实测 319.7,富途同值)。不能用 range=5d:其 chartPreviousClose
|
|
70
|
+
* 锚在窗口首根前一收盘(≈6 个交易日前);bars[len-2] 兜底也不可靠——日 K 序列存在
|
|
71
|
+
* 「最新收盘交易日滞后补齐/跳缺」的 vintage 行为(模块头注记),缺口期会错位一个交易日。
|
|
72
|
+
* 日线滞后窗口内 bars 可能为空:仅凭 meta 仍能出全量快照,不再因缺 bar 抛错。
|
|
73
|
+
*/
|
|
74
|
+
getTicker(symbol: string): Promise<Ticker>;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { INTERVAL_VOCABULARY, TradingServiceError, YahooChartEnvelope, YahooChartResult, YahooRestClient, YahooRestOptions, isSupportedInterval, normalizeYahooSymbol, parseChartBars };
|
package/lib/rest.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
//#region src/rest.ts
|
|
2
|
+
/** api 包 TradingError 契约的运行时 Error 实现。 */
|
|
3
|
+
var TradingServiceError = class extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message, cause) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "TradingServiceError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
if (cause !== void 0) this.cause = cause;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/** Yahoo 符号:字母/数字或指数前导 `^` 开头,可含 `.` `-` `^`(ADR/优先股/指数写法)。 */
|
|
13
|
+
const YAHOO_SYMBOL_PATTERN = /^[A-Z0-9^][A-Z0-9.\-^]{0,15}$/;
|
|
14
|
+
/** 规范化并校验符号:接受 `AAPL` / `aapl` / `brk-b`,统一为大写;不补任何后缀。 */
|
|
15
|
+
function normalizeYahooSymbol(symbol) {
|
|
16
|
+
if (typeof symbol !== "string" || !symbol.trim()) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", "Symbol must be a non-empty string, e.g. AAPL or BRK-B");
|
|
17
|
+
const normalized = symbol.trim().toUpperCase();
|
|
18
|
+
if (!YAHOO_SYMBOL_PATTERN.test(normalized)) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Symbol ${JSON.stringify(symbol)} is not a valid Yahoo Finance symbol (expected like AAPL / BRK-B / 7203.T)`);
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
/** api Interval 词汇的受支持子集 → Yahoo `interval=` 值。 */
|
|
22
|
+
const INTERVAL_TO_YAHOO = /* @__PURE__ */ new Map([
|
|
23
|
+
["1m", "1m"],
|
|
24
|
+
["5m", "5m"],
|
|
25
|
+
["15m", "15m"],
|
|
26
|
+
["30m", "30m"],
|
|
27
|
+
["1h", "60m"],
|
|
28
|
+
["1d", "1d"],
|
|
29
|
+
["1w", "1wk"],
|
|
30
|
+
["1M", "1mo"]
|
|
31
|
+
]);
|
|
32
|
+
/** 工具 parameters enum 用:受支持 interval 词汇(与 connector-stooq 同一子集口径)。 */
|
|
33
|
+
const INTERVAL_VOCABULARY = [...INTERVAL_TO_YAHOO.keys()];
|
|
34
|
+
function isSupportedInterval(value) {
|
|
35
|
+
return INTERVAL_TO_YAHOO.has(value);
|
|
36
|
+
}
|
|
37
|
+
/** 每个 interval 的默认取数窗口(1m 上限 7 天;日线给足近三年——服务层再按 limit 截尾)。 */
|
|
38
|
+
const RANGE_BY_INTERVAL = /* @__PURE__ */ new Map([
|
|
39
|
+
["1m", "7d"],
|
|
40
|
+
["5m", "1mo"],
|
|
41
|
+
["15m", "1mo"],
|
|
42
|
+
["30m", "1mo"],
|
|
43
|
+
["1h", "3mo"],
|
|
44
|
+
["1d", "3y"],
|
|
45
|
+
["1w", "5y"],
|
|
46
|
+
["1M", "10y"]
|
|
47
|
+
]);
|
|
48
|
+
/** 单根 K 线名义时长(ms),closeTime = openTime + 时长 − 1。1M 按 30 天近似(月界不精确,工具语义足够)。 */
|
|
49
|
+
const INTERVAL_MS = /* @__PURE__ */ new Map([
|
|
50
|
+
["1m", 6e4],
|
|
51
|
+
["5m", 3e5],
|
|
52
|
+
["15m", 9e5],
|
|
53
|
+
["30m", 18e5],
|
|
54
|
+
["1h", 36e5],
|
|
55
|
+
["1d", 864e5],
|
|
56
|
+
["1w", 6048e5],
|
|
57
|
+
["1M", 2592e6]
|
|
58
|
+
]);
|
|
59
|
+
/** 把 timestamp[] + quote 序列对齐成 Kline[](旧→新;null 行如实丢弃)。 */
|
|
60
|
+
function parseChartBars(result, interval) {
|
|
61
|
+
const quote = result.indicators.quote[0] ?? {};
|
|
62
|
+
const periodMs = INTERVAL_MS.get(interval) ?? 864e5;
|
|
63
|
+
const bars = [];
|
|
64
|
+
for (let i = 0; i < result.timestamp.length; i++) {
|
|
65
|
+
const close = quote.close?.[i];
|
|
66
|
+
if (close == null) continue;
|
|
67
|
+
const openTime = result.timestamp[i] * 1e3;
|
|
68
|
+
bars.push({
|
|
69
|
+
openTime,
|
|
70
|
+
open: quote.open?.[i] ?? close,
|
|
71
|
+
high: quote.high?.[i] ?? close,
|
|
72
|
+
low: quote.low?.[i] ?? close,
|
|
73
|
+
close,
|
|
74
|
+
volume: quote.volume?.[i] ?? 0,
|
|
75
|
+
closeTime: openTime + periodMs - 1
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return bars;
|
|
79
|
+
}
|
|
80
|
+
const DEFAULT_BASE_URL = "https://query1.finance.yahoo.com";
|
|
81
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
82
|
+
/** 非官方 API 但需浏览器特征头;Mozilla/5.0 裸 UA 实测即可(2026-08-29 本出口)。 */
|
|
83
|
+
const DEFAULT_USER_AGENT = "Mozilla/5.0";
|
|
84
|
+
var YahooRestClient = class {
|
|
85
|
+
#baseUrl;
|
|
86
|
+
#timeoutMs;
|
|
87
|
+
#fetchImpl;
|
|
88
|
+
#userAgent;
|
|
89
|
+
constructor(options = {}) {
|
|
90
|
+
this.#baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
91
|
+
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
92
|
+
this.#fetchImpl = options.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
93
|
+
this.#userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
|
|
94
|
+
}
|
|
95
|
+
async #requestChart(symbol, interval, rangeOverride) {
|
|
96
|
+
const sym = normalizeYahooSymbol(symbol);
|
|
97
|
+
const yInterval = INTERVAL_TO_YAHOO.get(interval);
|
|
98
|
+
if (yInterval === void 0) throw new TradingServiceError("TRADING_UNSUPPORTED_INTERVAL", `Yahoo klines: unsupported interval ${String(interval)} — supported: ${INTERVAL_VOCABULARY.join("/")}`);
|
|
99
|
+
const range = rangeOverride ?? RANGE_BY_INTERVAL.get(interval) ?? "1y";
|
|
100
|
+
const query = new URLSearchParams({
|
|
101
|
+
interval: yInterval,
|
|
102
|
+
range
|
|
103
|
+
});
|
|
104
|
+
const target = `${this.#baseUrl}/v8/finance/chart/${encodeURIComponent(sym)}?${query}`;
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timer = setTimeout(() => controller.abort(new DOMException(`request timed out after ${this.#timeoutMs}ms`, "TimeoutError")), this.#timeoutMs);
|
|
107
|
+
let res;
|
|
108
|
+
try {
|
|
109
|
+
res = await this.#fetchImpl(target, {
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
headers: {
|
|
112
|
+
accept: "application/json",
|
|
113
|
+
"user-agent": this.#userAgent
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
const timedOut = controller.signal.aborted;
|
|
118
|
+
throw new TradingServiceError("TRADING_NETWORK", timedOut ? `Yahoo chart ${sym}: request timed out after ${this.#timeoutMs}ms` : `Yahoo chart ${sym}: network error`, cause);
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
if (!res.ok) throw new TradingServiceError(res.status === 429 ? "TRADING_RATE_LIMITED" : "TRADING_EXCHANGE_ERROR", `Yahoo chart ${sym}: HTTP ${res.status} ${res.statusText}`);
|
|
123
|
+
let envelope;
|
|
124
|
+
try {
|
|
125
|
+
envelope = await res.json();
|
|
126
|
+
} catch (cause) {
|
|
127
|
+
throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Yahoo chart ${sym}: response is not valid JSON`, cause);
|
|
128
|
+
}
|
|
129
|
+
if (envelope.chart.error) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Yahoo chart ${sym}: upstream error ${envelope.chart.error.code ?? ""} ${envelope.chart.error.description ?? ""}`.trim());
|
|
130
|
+
const result = envelope.chart.result?.[0];
|
|
131
|
+
if (!result || !Array.isArray(result.timestamp) || result.timestamp.length === 0) throw new TradingServiceError("TRADING_UNSUPPORTED_SYMBOL", `Yahoo chart ${sym}: no data rows (unknown/delisted symbol?)`);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
/** 拉取并解析 K 线(旧→新全窗口,由服务层按 limit 截尾)。 */
|
|
135
|
+
async getKlines(symbol, interval) {
|
|
136
|
+
const sym = normalizeYahooSymbol(symbol);
|
|
137
|
+
const bars = parseChartBars(await this.#requestChart(sym, interval), interval);
|
|
138
|
+
if (bars.length === 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Yahoo klines for ${sym}: no non-null bars in range`);
|
|
139
|
+
return bars;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 最新行情快照:单请求 interval=1d&range=1d。
|
|
143
|
+
* price/time/volume/prevClose 全取 meta(权威实时面):昨收锚点取 meta.chartPreviousClose
|
|
144
|
+
* ——range=1d 窗口恰只含最新一个会话,其「窗口首根前一收盘」即官方昨收,与富途等终端
|
|
145
|
+
* 同语义(2026-09-01 AAPL 实测 319.7,富途同值)。不能用 range=5d:其 chartPreviousClose
|
|
146
|
+
* 锚在窗口首根前一收盘(≈6 个交易日前);bars[len-2] 兜底也不可靠——日 K 序列存在
|
|
147
|
+
* 「最新收盘交易日滞后补齐/跳缺」的 vintage 行为(模块头注记),缺口期会错位一个交易日。
|
|
148
|
+
* 日线滞后窗口内 bars 可能为空:仅凭 meta 仍能出全量快照,不再因缺 bar 抛错。
|
|
149
|
+
*/
|
|
150
|
+
async getTicker(symbol) {
|
|
151
|
+
const sym = normalizeYahooSymbol(symbol);
|
|
152
|
+
const result = await this.#requestChart(sym, "1d", "1d");
|
|
153
|
+
const bars = parseChartBars(result, "1d");
|
|
154
|
+
const last = bars[bars.length - 1];
|
|
155
|
+
const price = result.meta.regularMarketPrice ?? last?.close;
|
|
156
|
+
if (price === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Yahoo ticker for ${sym}: no price in meta and no bars`);
|
|
157
|
+
const prevClose = result.meta.chartPreviousClose ?? result.meta.previousClose ?? (bars.length >= 2 ? bars[bars.length - 2]?.close : void 0);
|
|
158
|
+
const changePercent = typeof result.meta.regularMarketChangePercent === "number" ? result.meta.regularMarketChangePercent : prevClose !== void 0 && prevClose > 0 ? (price - prevClose) / prevClose * 100 : void 0;
|
|
159
|
+
const timestamp = result.meta.regularMarketTime != null ? result.meta.regularMarketTime * 1e3 : last?.closeTime;
|
|
160
|
+
if (timestamp === void 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Yahoo ticker for ${sym}: no timestamp in meta and no bars`);
|
|
161
|
+
const name = result.meta.shortName ?? result.meta.longName;
|
|
162
|
+
const volume = result.meta.regularMarketVolume ?? last?.volume;
|
|
163
|
+
return {
|
|
164
|
+
symbol: sym,
|
|
165
|
+
...name ? { name } : {},
|
|
166
|
+
price,
|
|
167
|
+
timestamp,
|
|
168
|
+
...volume !== void 0 ? { volume } : {},
|
|
169
|
+
...prevClose !== void 0 ? { prevClose } : {},
|
|
170
|
+
...changePercent !== void 0 ? { changePercent } : {}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
//#endregion
|
|
175
|
+
export { INTERVAL_VOCABULARY, TradingServiceError, YahooRestClient, isSupportedInterval, normalizeYahooSymbol, parseChartBars };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dshtrading/connector-yahoo",
|
|
3
|
+
"description": "US market connector plugin for dsh-trading: public market data from the Yahoo Finance v8 chart API implementing the @dshtrading/api contracts (unofficial API, no key; personal use per Yahoo terms of use)",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
"./dataplane": {
|
|
15
|
+
"types": "./lib/dataplane.d.ts",
|
|
16
|
+
"default": "./lib/dataplane.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib"
|
|
21
|
+
],
|
|
22
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@dshtrading/api": "^0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@deepseek-ai/cordis": ">=4.0.0",
|
|
28
|
+
"@deepseek-ai/dsh-tools": ">=0.1.2-alpha.1",
|
|
29
|
+
"@deepseek-ai/schemastery": ">=3.18.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsdown": "^0.22.0",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"test": "vitest run"
|
|
38
|
+
}
|
|
39
|
+
}
|