时间与交易时段

TradingCalendar 把交易所的日程展开成 UTC 时间轴上一个个具体的交易时段,该层的其余部分都以它为基础:柱体时钟据此计算当前 K 线何时收盘,坐标轴格式化器据此准备刻度和十字线的标签。该层本身不绘制任何东西——它回答的是“现在是否在交易”“下一个时段何时开始”“距离柱体收盘还有多久”这类问题。

引入

入口点是 @stocksharp/chart/time;同样的名称也可以从包的根 @stocksharp/chart 获取。API 中的所有时间都是以秒为单位的 Unix 时间,区间是半开的:[openTime, closeTime)

import {
  TradingCalendar,
  TradingSessionKind,
  type TradingSchedule,
} from '@stocksharp/chart/time';

创建日历

日程描述交易所的时区(IANA)、每周的时段规则,以及必要时的完整休市日期和特定日期的日程替换。规则中的时间是交易所的本地时间;日历会自行把它换算为 UTC,并考虑夏令时切换。

import {
  TradingCalendar,
  TradingSessionKind,
  type TradingSchedule,
} from '@stocksharp/chart/time';

const schedule: TradingSchedule = {
  id: 'xnys',
  timeZone: 'America/New_York',
  sessions: [
    {
      id: 'pre',
      kind: TradingSessionKind.PreMarket,
      weekdays: [1, 2, 3, 4, 5],
      open: { hour: 4, minute: 0 },
      close: { hour: 9, minute: 30 },
    },
    {
      id: 'regular',
      kind: TradingSessionKind.Regular,
      weekdays: [1, 2, 3, 4, 5],
      open: { hour: 9, minute: 30 },
      close: { hour: 16, minute: 0 },
    },
    {
      id: 'post',
      kind: TradingSessionKind.PostMarket,
      weekdays: [1, 2, 3, 4, 5],
      open: { hour: 16, minute: 0 },
      close: { hour: 20, minute: 0 },
    },
  ],
  holidays: ['2026-01-01', '2026-07-03'],
  overrides: [
    {
      date: '2026-11-27',                                  // 感恩节之后的半日市
      sessions: [{
        id: 'regular',
        kind: TradingSessionKind.Regular,
        open: { hour: 9, minute: 30 },
        close: { hour: 13, minute: 0 },
      }],
    },
  ],
};

const calendar = new TradingCalendar(schedule);
const now = Math.floor(Date.now() / 1000);

calendar.isTradingTime(now, [TradingSessionKind.Regular]);   // 主交易时段是否正在进行
calendar.sessionAt(now);                                     // 当前时段,没有则为 null
calendar.nextSession(now, [TradingSessionKind.Regular]);     // 最近一次开盘
calendar.sessionsInRange({ from: now - 86_400, to: now });   // 这一天内的所有时段

构造函数会立即校验并规范化日程:时区必须能被 Intl 识别,日期必须写成 YYYY-MM-DD,时段标识符必须唯一,而时段本身无论在一周之内还是展开到 UTC 之后都不得重叠。时段的时长必须为正且不超过 24 小时,同一个日期不能既是休市日又是替换日。违反任何一条都会在创建日历时就抛出 TypeErrorRangeError。解析过的本地日期会被缓存,因此对同一区间的重复查询不会重新计算。

日程与时段

规则 TradingSessionRule 就是模板 TradingSessionTemplate 加上按 ISO 编号的星期几(1 为星期一,7 为星期日)。星期几针对的是时段的开始日期;对于在下一个本地日才收盘的夜盘时段,需要设置 closeDayOffset: 1

holidays 会完全封闭某个日期——任何规则都不会展开到这一天。overrides 会在指定日期用所列的一组时段替换全部重复性时段,这对半日市很方便。

每次查询返回的都是已实例化的 TradingSession:形如 <日期>/<规则 id>idruleIdkind、交易日期 tradingDate、以 UTC 表示的边界 openTimecloseTime,以及标志 isOverride。所有方法中的可选参数 kinds 都会按时段类型(pre-marketregularpost-market)过滤结果。

夏令时切换被显式处理:落在含糊(重复出现)小时的开盘取较早的偏移,收盘取较晚的偏移,而落在春季调时被跳过的那一小时里的本地时间,会被移到第一个真实存在的时刻。nextSessionpreviousSession 的搜索被限制在所给时刻起十年之内;找不到时段时返回 null

柱体倒计时

resolveTradingBarBounds 按柱体的开始时间确定它的边界,而 calculateBarCountdown 据此构建倒计时快照。当前时间由调用方传入——该层不自建计时器,因此结果是确定性的,适合用于测试。

import {
  BarClockState,
  calculateBarCountdown,
  resolveTradingBarBounds,
} from '@stocksharp/chart/time';

const bounds = resolveTradingBarBounds(barOpenTime, '15m', { calendar });
// bounds: { resolution, intervalSeconds, openTime, closeTime, durationSeconds, session }

const countdown = calculateBarCountdown(
  barOpenTime,
  '15m',
  Math.floor(Date.now() / 1000),
  { calendar },
);

if (countdown !== null && countdown.state === BarClockState.Open) {
  console.log(countdown.remainingSeconds, countdown.progress);
}

时间框架写成一个数字加上后缀 smhdw30s5m1h1d1w);没有后缀时数值按分钟计算,不支持自然月。没有日历时,柱体的收盘时间就是开始时间加上时间框架的时长。有日历时,日内柱体会被自己所属时段的收盘时间截断,日线柱体按交易日推进,而周线柱体按 ISO 日历周推进。如果开始时间不落在任何时段内,这两个函数都返回 null

参数 sessionKinds 指定要考虑哪些时段;默认只取主时段(regular),而在没有 calendar 时使用它会报错。BarCountdown 包含状态 pending / open / closed、时间 now、边界 bounds,以及 untilOpenSecondselapsedSecondsremainingSeconds 和柱体已完成的比例 progress

时间轴标签

TimeAxisFormatter 是对 Intl.DateTimeFormat 的带缓存封装,刻度标签和十字线标签共用它。

import { TimeAxisFormatter } from '@stocksharp/chart/time';

const formatter = new TimeAxisFormatter({
  locale: 'zh-CN',
  timeZone: 'Asia/Shanghai',
  timeVisible: true,
  secondsVisible: false,
});

formatter.formatTick(now, 3_600);   // 步长为一小时时的刻度标签
formatter.formatCrosshair(now);     // 十字线下方的标签

以秒为单位的网格步长决定标签的详细程度:年、月、日或一天中的时间。只有在 secondsVisible 为真且步长小于一分钟时才会出现秒。默认使用 en-GB 区域设置和 UTC 时区;这两个值都会通过 Intl 规范化,并可通过属性 localetimeZone 获取。

可选的 formatter 会接管这两种标签,并收到时间和上下文 TimeScaleFormatContext——标签的种类(tickcrosshair)、区域设置、时区、标志 timeVisiblesecondsVisible,以及步长 tickStep(对十字线而言它等于 null)。如果该函数返回的不是字符串或抛出了异常,就使用内置格式。

接入图表

图表可以直接接受日历:刻度模式 session-aware 会把坐标轴压缩到交易时间,而原语 SessionShading 会用面板背景高亮各时段。

import { createChart, SessionShading, TimeScaleMode } from '@stocksharp/chart';
import { TradingSessionKind } from '@stocksharp/chart/time';

const chart = createChart(document.getElementById('chart')!, {
  timeScale: {
    mode: TimeScaleMode.SessionAware,
    calendar,
    sessionKinds: [TradingSessionKind.Regular],
    timeVisible: true,
  },
});

chart.attachPrimitive(new SessionShading({ calendar }));

没有日历就无法创建 session-aware 模式——这属于配置错误。如果没有指定 timeScale.timeZone,就取日历的时区,日历也没有时则取 UTC。省略 sessionKinds 表示坐标轴上保留所有类型的时段。

公共方法

TradingCalendar 实现 ITradingCalendar

  • schedule() — 规范化之后的日程。
  • sessionsInRange(range, kinds?) — 与该区间相交的时段,按开始时间升序。
  • sessionAt(time, kinds?) — 包含该时刻的时段,否则为 null
  • isTradingTime(time, kinds?) — 该时刻是否正在交易。
  • nextSession(time, kinds?) — 不早于指定时刻开始的最近一个时段。
  • previousSession(time, kinds?) — 不晚于指定时刻结束的最后一个时段。

柱体时钟的函数:

  • resolveTradingBarBounds(barOpenTime, resolution, options?) — 柱体的边界。
  • calculateBarCountdown(barOpenTime, resolution, now, options?) — 倒计时快照。

TimeAxisFormatter

  • formatTick(time, step) — 给定步长下的刻度标签。
  • formatCrosshair(time) — 十字线标签。
  • localetimeZone — 规范化之后的值,只读。

该层的其余导出是类型和常量集合:TradingSessionKindBarClockStateTimeScaleLabelKindITradingCalendarTradingScheduleTradingSessionRuleTradingSessionTemplateTradingDayOverrideTradingSessionBarClockOptionsTradingBarBoundsBarCountdownTimeAxisFormatterOptionsTimeScaleFormatterTimeScaleFormatContextIsoWeekdayLocalDateLocalTimeOfDay

另请参阅