# KLine Charts

<p align="center">
  <strong>📈 专业级股票 K 线图表 React 组件</strong>
</p>

<p align="center">
  基于 ECharts，内置 stock-sdk 数据源，开箱即用
</p>

<p align="center">
  <a href="#安装">安装</a> •
  <a href="#快速开始">快速开始</a> •
  <a href="#功能特性">功能特性</a> •
  <a href="#api-文档">API 文档</a> •
  <a href="#主题定制">主题定制</a>
</p>

---

## 功能特性

- 🚀 **零配置数据** - 内置 stock-sdk，传入股票代码自动获取数据
- 🔌 **可插拔数据源** - 支持自定义 DataProvider（解决跨域/接入自有行情源/SSR）
- 🌍 **多市场支持** - A 股、港股、美股
- 📊 **丰富周期** - 分时、五日、日 K、周 K、月 K、分钟级 K 线
- 📈 **技术指标** - MA/MACD/BOLL/KDJ/RSI/WR/BIAS/CCI/ATR/OBV/ROC/DMI/SAR/KC
- 🎯 **交互完善** - 缩放、平移、十字准线、Tooltip、撤销/重做
- 🖥️ **全屏模式** - 一键全屏展示
- 🔄 **自动刷新** - 分时模式支持自动刷新数据
- 🎨 **高度可定制** - 主题、颜色、指标参数均可配置
- 📱 **响应式设计** - 自适应容器尺寸
- 📦 **轻量体积** - Tree-shaking 友好，按需引入 ECharts 组件

## 安装

```bash
npm install kline-charts-react

# 或使用 yarn
yarn add kline-charts-react

# 或使用 pnpm
pnpm add kline-charts-react
```

### Peer Dependencies

```bash
npm install react react-dom echarts
```

## 快速开始

```tsx
import { KLineChart } from 'kline-charts-react';
import 'kline-charts-react/style.css';

function App() {
  return (
    <KLineChart
      symbol="sh600519"  // 贵州茅台
      height={600}
    />
  );
}
```

## 基础用法

### 切换股票

```tsx
<KLineChart
  symbol="sz000001"  // 平安银行
  market="A"         // A 股（默认）
/>
```

### 指定周期

```tsx
<KLineChart
  symbol="sh600519"
  defaultPeriod="weekly"  // 非受控模式下默认显示周 K
/>
```

可选周期：`timeline` | `timeline5` | `1` | `5` | `15` | `30` | `60` | `daily` | `weekly` | `monthly`

### 受控 / 非受控

`period`、`adjust`、`indicators` 支持受控模式；`defaultPeriod`、`defaultAdjust`、`defaultIndicators` 用于非受控默认值。

```tsx
// 非受控
<KLineChart
  symbol="sh600519"
  defaultPeriod="daily"
  defaultAdjust="qfq"
  defaultIndicators={['ma', 'volume', 'macd']}
/>
```

```tsx
// 受控
function App() {
  const [period, setPeriod] = useState<PeriodType>('daily');

  return (
    <KLineChart
      symbol="sh600519"
      period={period}
      onPeriodChange={setPeriod}
    />
  );
}
```

### 配置技术指标

```tsx
<KLineChart
  symbol="sh600519"
  indicators={['ma', 'volume', 'macd', 'kdj']}
  indicatorOptions={{
    ma: { periods: [5, 10, 20, 60] },
    macd: { short: 12, long: 26, signal: 9 },
  }}
/>
```

### 深色主题

```tsx
<KLineChart
  symbol="sh600519"
  theme="dark"
/>
```

### 自动刷新（分时模式）

```tsx
<KLineChart
  symbol="sh600519"
  period="timeline"
  autoRefresh={{ intervalMs: 5000, onlyTradingTime: true }}
/>
```

### 自定义数据源

解决跨域问题或接入自有行情源：

```tsx
import { KLineChart, type KLineDataProvider } from 'kline-charts-react';

const customProvider: KLineDataProvider = {
  // K 线数据（必须实现）
  getKline: async (params, signal) => {
    // params: { symbol, market, period, adjust, cursor?, limit? }
    // cursor/limit 仅在「向左滚动加载更多历史」时传入，见下方「无限滚动加载历史」
    const res = await fetch(`/api/kline?symbol=${params.symbol}&period=${params.period}`, { signal });
    return res.json(); // 返回 KlineData[]
  },
  // 分时数据（可选，period 为 'timeline' / 'timeline5' 时使用）
  getTimeline: async (params, signal) => {
    // params.period: 'timeline'（单日，默认）| 'timeline5'（五日）
    const days = params.period === 'timeline5' ? 5 : 1;
    const res = await fetch(`/api/timeline?symbol=${params.symbol}&days=${days}`, { signal });
    const json = await res.json();
    // 推荐返回 { data, prevClose }：prevClose（昨收）用于分时图的涨跌着色与昨收参考线
    return { data: json.data, prevClose: json.prevClose };
    // 也兼容直接 return TimelineData[]（此时无昨收线）
  },
};

<KLineChart
  symbol="sh600519"
  dataProvider={customProvider}
/>
```

#### KlineData 数据结构

`getKline` 需要返回 `KlineData[]`，每条数据的字段如下：

```ts
interface KlineData {
  date: string;              // 日期/时间，如 "2024-01-15" 或 "2024-01-15 09:35"
  open: number | null;       // 开盘价
  close: number | null;      // 收盘价
  high: number | null;       // 最高价
  low: number | null;        // 最低价
  volume: number | null;     // 成交量
  amount: number | null;     // 成交额
  changePercent?: number;    // 涨跌幅（可选）
  change?: number;           // 涨跌额（可选）
  amplitude?: number;        // 振幅（可选）
  turnoverRate?: number;     // 换手率（可选）
}
```

#### TimelineData 数据结构

`getTimeline` 推荐返回 `TimelineResult`（`{ data, prevClose }`），其中 `data` 为分时序列、`prevClose` 为昨收价；也兼容直接返回 `TimelineData[]`：

```ts
interface TimelineData {
  time: string;       // 时间："09:30" 或 "2024-01-15 09:30" 均可（渲染层两种都支持；
                      // 内置源：A 股单日分时为 HH:mm，港/美股单日与所有五日分时带日期前缀）
  price: number;      // 当前价格
  volume: number;     // 累计成交量（按交易日累计）
  amount: number;     // 累计成交额（按交易日累计）
  avgPrice: number;   // 均价
}

interface TimelineResult {
  data: TimelineData[];
  prevClose?: number | null;  // 昨收价：用于分时图涨跌着色与昨收参考线，缺省则不绘制昨收线
}
```

> 技术指标（MA、MACD、KDJ 等）会由组件自动根据 K 线数据计算，无需在数据源中提供。
> 未实现 `getTimeline` 的自定义数据源：分时/五日分时会退化为「调用 `getKline`
> 拿分钟数据 + 组件内聚合 VWAP」，此时无昨收基准。

### 无限滚动加载历史（loadMore）

日/周/月 K 向左滚动到最左侧时，组件会调用 `getKline` 并带上
`cursor`（当前最早一根的 date）与 `limit`（默认 180，可用
`requestOptions.loadMoreLimit` 配置）请求更早的历史：

```tsx
const provider: KLineDataProvider = {
  getKline: async ({ symbol, period, cursor, limit }, signal) => {
    const qs = cursor ? `&before=${cursor}&limit=${limit}` : '';
    const res = await fetch(`/api/kline?symbol=${symbol}&period=${period}${qs}`, { signal });
    return res.json();
  },
};
```

> ⚠️ **内置数据源不支持增量加载历史**：默认 provider 一次性返回全部历史并忽略
> `cursor`/`limit`，向左滚动不会加载更多（首次触发后自动停止探测）。
> 无限滚动仅在自定义 `dataProvider` 实现了基于 cursor 的分页时生效。
>
> 分页返回的顺序不限（newest-first / oldest-first 均可）：组件会去重并按日期
> 升序归并后再渲染。

### 使用 Ref 控制图表

```tsx
import { useRef } from 'react';
import { KLineChart, type KLineChartRef } from 'kline-charts-react';

function App() {
  const chartRef = useRef<KLineChartRef>(null);

  const handleRefresh = () => {
    chartRef.current?.refresh();
  };

  const handleExport = () => {
    const dataUrl = chartRef.current?.exportImage('png');
    // 下载图片...
  };

  return (
    <>
      <button onClick={handleRefresh}>刷新</button>
      <button onClick={handleExport}>导出图片</button>
      <KLineChart ref={chartRef} symbol="sh600519" />
    </>
  );
}
```

### `unstable` 二级导出

主入口只保留稳定导出。如果你明确需要内部 hooks / 子组件，请从 `kline-charts-react/unstable` 引入：

```tsx
import { useKlineData, Toolbar } from 'kline-charts-react/unstable';
```

## API 文档

### KLineChartProps

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `symbol` | `string` | **必填** | 股票代码，如 `sh600519`、`sz000001` |
| `market` | `'A' \| 'HK' \| 'US'` | `'A'` | 市场类型 |
| `period` | `PeriodType` | - | 当前 K 线周期（受控） |
| `defaultPeriod` | `PeriodType` | `'daily'` | 非受控模式下默认周期 |
| `adjust` | `'' \| 'qfq' \| 'hfq'` | - | 当前复权类型（受控） |
| `defaultAdjust` | `'' \| 'qfq' \| 'hfq'` | `'qfq'` | 非受控模式下默认复权 |
| `height` | `number \| string` | `500` | 图表高度 |
| `width` | `number \| string` | `'100%'` | 图表宽度 |
| `theme` | `'light' \| 'dark' \| ThemeConfig` | `'light'` | 主题配置 |
| `indicators` | `IndicatorType[]` | - | 当前启用的技术指标（受控） |
| `defaultIndicators` | `IndicatorType[]` | `['ma', 'volume', 'macd']` | 非受控模式下默认指标 |
| `indicatorOptions` | `IndicatorOptions` | - | 指标参数配置 |
| `showToolbar` | `boolean` | `true` | 是否显示工具栏 |
| `showPeriodSelector` | `boolean` | `true` | 是否显示周期切换 |
| `showIndicatorSelector` | `boolean` | `true` | 是否显示指标切换 |
| `maxSubPanes` | `number` | `3` | 最多显示几个副图，0 表示不显示副图 |
| `visibleCount` | `number` | `60` | 初始可见 K 线数量（关闭缩放的周期不生效，显示全部） |
| `showDataZoomSlider` | `boolean \| Partial<Record<PeriodType, boolean>>` | `true` | 是否启用缩放（滚轮 + 底部滑块），可按周期设置，如 `{ timeline: false }`；关闭后该周期无缩放/平移、`visibleCount` 与滚动加载历史均不生效 |
| `dataProvider` | `KLineDataProvider` | - | 自定义数据源（请用 `useMemo` 保持引用稳定） |
| `sdkOptions` | `SDKOptions` | - | stock-sdk 配置 |
| `requestOptions` | `RequestOptions` | - | 请求控制配置（见下表） |
| `autoRefresh` | `boolean \| AutoRefreshOptions` | - | 自动刷新配置 |
| `echartsOption` | `EChartsOption` | - | 自定义 ECharts 配置 |
| `echartsOptionMerge` | `EChartsOptionMergeOptions` | - | ECharts Option 合并策略 |
| `panes` | `PaneConfig[]` | - | 自定义面板布局，支持单副图多指标；主图面板的 `id` 必须为 `'main'` |
| `onDataLoad` | `(data: KlineWithIndicators[]) => void` | - | 数据加载回调（仅 K 线周期触发，数据已附带指标字段） |
| `onPeriodChange` | `(period: PeriodType) => void` | - | 周期切换回调 |
| `onAdjustChange` | `(adjust: AdjustType) => void` | - | 复权切换回调 |
| `onIndicatorsChange` | `(indicators: IndicatorType[]) => void` | - | 指标切换回调 |
| `onVisibleRangeChange` | `({ start, end }) => void` | - | 当前 dataZoom 可见范围变化 |
| `onError` | `(error: Error) => void` | - | 错误回调 |

### RequestOptions

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `debounceMs` | `number` | `150` | 参数变化后的请求防抖时间 |
| `abortOnChange` | `boolean` | `true` | 参数变化时是否中止上一次请求（内置数据源的网络层不支持中止，仅丢弃过期结果） |
| `dedupe` | `boolean` | `true` | 相同请求（同数据源 + 同参数）是否走缓存与去重 |
| `loadMoreLimit` | `number` | `180` | 滚动加载历史时每次请求的条数（传给 `getKline` 的 `limit`） |

### KLineChartRef

| 方法 | 说明 |
|------|------|
| `refresh()` | 刷新数据 |
| `setPeriod(period)` | 切换周期 |
| `setAdjust(adjust)` | 切换复权 |
| `setIndicators(indicators)` | 切换指标 |
| `zoomTo(start, end)` | 缩放到指定范围 |
| `resetZoom()` | 重置缩放 |
| `getVisibleRange()` | 获取当前可见范围 `{ start, end }` |
| `getEchartsInstance()` | 获取 ECharts 实例，未初始化时返回 `null` |
| `exportImage(type?)` | 导出图片（png/jpeg），图表实例未就绪时返回 `null` |
| `getData()` | 获取当前 K 线数据（`KlineWithIndicators[]`，已附带指标字段） |

### PeriodType

```ts
type PeriodType =
  | 'timeline'   // 分时
  | 'timeline5'  // 五日分时
  | '1'          // 1分钟
  | '5'          // 5分钟
  | '15'         // 15分钟
  | '30'         // 30分钟
  | '60'         // 60分钟
  | 'daily'      // 日K
  | 'weekly'     // 周K
  | 'monthly';   // 月K
```

#### 内置数据源的市场 × 周期支持

| 周期 | A 股 | 港股 | 美股 | 说明 |
|------|:---:|:---:|:---:|------|
| 分时 `timeline` | ✅ | ✅ | ✅ | A 股走行情分时接口（自带昨收）；港/美股由 1 分钟数据聚合，昨收取自日 K |
| 五日分时 `timeline5` | ✅ | ✅ | ✅ | 由 1 分钟数据按交易日聚合（VWAP 均价按日重置），昨收取自窗口前最近交易日的日 K 收盘 |
| 分钟 `1/5/15/30/60` | ✅ | ✅ | ✅ | 按市场路由到对应分钟 K 线端点 |
| 日/周/月 | ✅ | ✅ | ✅ | 支持前复权/后复权/不复权 |

> 使用自定义 `dataProvider` 时不受上表限制，支持范围由你的数据源决定。

**代码格式**（内置数据源）：A 股带市场前缀如 `sh600519` / `sz000001`；港股用 5 位
数字如 `00700`；美股需带交易所前缀，如 `105.AAPL`（105=纳斯达克、106=NYSE、107=AMEX）。

### IndicatorType

```ts
type IndicatorType =
  // 主图指标
  | 'ma'      // 移动平均线
  | 'boll'    // 布林带
  | 'sar'     // 抛物线转向（SAR）
  | 'kc'      // 肯特纳通道（KC）
  // 副图指标
  | 'volume'  // 成交量
  | 'macd'    // MACD
  | 'kdj'     // KDJ
  | 'rsi'     // RSI
  | 'wr'      // WR（威廉指标）
  | 'bias'    // BIAS（乖离率）
  | 'cci'     // CCI（顺势指标）
  | 'atr'     // ATR（平均真实波幅）
  | 'obv'     // OBV（能量潮）
  | 'roc'     // ROC（变动率）
  | 'dmi';    // DMI（趋向指标）
```

**主图指标说明**：
- **MA** - 移动平均线，显示不同周期的均线
- **BOLL** - 布林带，由上轨、中轨、下轨组成的通道
- **SAR** - 抛物线转向，以点状显示趋势反转信号
- **KC** - 肯特纳通道，基于 EMA 和 ATR 的通道指标

**副图指标说明**：
- **OBV** - 能量潮，通过成交量变化预测价格趋势
- **ROC** - 变动率，衡量价格变化的速度
- **DMI** - 趋向指标，包含 +DI、-DI、ADX、ADXR 四条线

## 主题定制

### 内置主题

```tsx
// 浅色主题（默认）
<KLineChart theme="light" />

// 深色主题
<KLineChart theme="dark" />
```

### 自定义主题

```tsx
<KLineChart
  theme={{
    backgroundColor: '#1a1a2e',
    textColor: '#eaeaea',
    upColor: '#26a69a',
    downColor: '#ef5350',
    gridLineColor: '#2a2a4a',
    // ... 更多配置
  }}
/>
```

## 处理跨域

内置数据源（stock-sdk v2）请求东方财富 / 雪球等多个行情端点，这些端点**通常允许
浏览器直连**，多数场景无需任何代理配置（本仓库 playground 即为直连）。

如果你的部署环境仍遇到跨域拦截（例如目标端点策略变化、内网安全网关拦截），推荐：

### 自定义 DataProvider（推荐）

用自己的后端聚合/代理行情数据，前端只对同源接口取数——这同时解决跨域、鉴权与
数据合规问题：

```tsx
<KLineChart
  symbol="sh600519"
  dataProvider={myDataProvider}  // 实现见上方「自定义数据源」
/>
```

> 说明：`sdkOptions.baseUrl` 在 stock-sdk v2 中**只影响腾讯快照行情端点**，
> 不会改写 K 线 / 分时等其余多数据源（eastmoney / xueqiu）的请求地址，
> **不能**用作跨域网关方案；旧版针对 `qt.gtimg.cn` 的 `/qt` 单目标代理同样已失效。
> 需要收口出网流量时请使用方案 1（自定义 DataProvider）。

## 目录结构

```
kline-charts-react/
├── src/
│   ├── components/           # 子组件
│   │   ├── Loading/          # 加载状态
│   │   ├── PeriodSelector/   # 周期切换
│   │   ├── IndicatorSelector/# 指标选择器
│   │   ├── Toolbar/          # 工具栏
│   │   ├── IndicatorDisplay/ # 主图指标数值
│   │   └── SubPaneTitle/     # 副图标题
│   ├── hooks/                # React Hooks
│   │   ├── useKlineData.ts   # 数据获取 & 指标计算
│   │   ├── useEcharts.ts     # ECharts 实例管理
│   │   └── useZoomHistory.ts # 缩放历史
│   ├── utils/                # 工具函数
│   │   ├── indicators.ts     # 技术指标计算
│   │   ├── optionBuilder.ts  # K 线 ECharts 配置
│   │   ├── timelineBuilder.ts# 分时图 ECharts 配置
│   │   ├── formatters.ts     # 数据格式化
│   │   └── cache.ts          # 数据缓存
│   ├── types/                # 类型定义
│   ├── KLineChart.tsx        # 主组件
│   └── index.ts              # 入口
├── playground/               # 调试环境
└── dist/                     # 构建产物
```

## 开发

```bash
# 安装依赖
yarn install

# 启动开发调试（直连本地源码，支持热更新）
yarn dev

# 构建组件库
yarn build

# 构建 playground 生产版本
yarn build:playground

# 代码检查
yarn lint

# 类型检查
yarn typecheck

# 单元测试
yarn test:run

# 打包校验
yarn pack:check
```

**开发模式说明**：
- `yarn dev` 会启动 playground，直接引用 `src/` 下的源码，修改源码后自动热更新
- `playground/` 通过本地 `file:..` 依赖引用当前仓库，避免和 npm 上的历史版本漂移
- CI 会执行 `lint`、`typecheck`、`test:run`、`build`、`build:playground` 和 `pack:check`

## 已知限制

- **导出图片**：`exportImage()` 导出的图片仅包含 ECharts 图表内容，不包含左上角的指标数值文字（这部分是用 React 渲染在图表外部的）
- **成交量单位**：内置数据源的 `volume` 原样透传上游数值，不同市场/端点的单位可能不同（股 vs 手）。若用于自定义计算或展示换算，请先用真实数据校准
- **自动刷新与节假日**：`autoRefresh.onlyTradingTime` 仅按「星期 + 交易时段」判断，不含法定节假日/半日市日历——休市日（非周末）仍会按周期发起刷新
- **请求中止**：内置数据源（stock-sdk v2）的接口不支持逐请求 AbortSignal，`requestOptions.abortOnChange` 对它只在结果层丢弃过期数据，不会真正取消网络请求；自定义 provider 透传 `signal` 即可获得真正的中止
- **无限滚动**：内置数据源不支持 `cursor`/`limit` 增量分页，向左滚动加载更多历史仅对实现了分页的自定义 provider 生效
- **onDataLoad**：仅 K 线周期触发；分时/五日分时的数据经由 `getTimeline` 通道，不回调 `onDataLoad`
- **同参数刷新失败的提示**：手动/自动刷新（参数不变）失败时会**保留旧图**、不弹内置错误层（避免遮挡与闪烁），仅触发 `onError` 回调——需要显式提示时请在 `onError` 里自行展示

## License

MIT © 2025-present [chengzuopeng](https://github.com/chengzuopeng)
