单元 01:认识市场与行情
单元 00 已经把一份 CSV 读进了 DataFrame。现在我们要回答一个更基本的问题:表格中的每一行究竟在描述什么?
本单元仍然使用固定的 DEMO_ETF 合成数据。你会逐列读懂日线行情,运行程序检查字段之间的关系,并生成第一张价格与成交量图。我们暂时不计算收益,也不设计策略。
本单元目标
完成本单元后,你将能够:
- 区分股票、指数、基金和 ETF;
- 用“市场 + 证券代码 + 货币”准确描述一个标的;
- 解释 OHLCV 每个字段的金融含义、单位和时间口径;
- 区分自然日、交易日与日线;
- 从图中识别开盘价、收盘价、日内最高最低区间和成交量;
- 解释不复权、前复权与后复权的用途和局限;
- 对一份行情数据作出准确的口径声明。
最终产物包括一份行情口径说明,以及由下面命令生成的行情图:
uv run python examples/unit01/inspect_ohlcv.py前置条件
- 已完成单元 00;
- 已执行
uv sync --locked; - 知道 CSV、DataFrame 和 Python 脚本分别是什么;
- 后续命令均在项目根目录运行。
场景:一行行情不是一个价格
先看样例数据的第一行:
DEMO_ETF,2024-01-02,100.00,101.20,99.80,100.80,1000000它并不是说“DEMO_ETF 在这一天的价格是某个值”,而是在压缩描述整个交易日:这一天从哪里开盘,交易过程中最高和最低到过哪里,最后以什么价格收盘,总共成交了多少份。
日线省略了什么
这种压缩很有用,也有代价。只看日线,我们不知道最高价发生在最低价之前还是之后,也不知道日内价格走过的完整路径。两个交易日完全可能拥有相同的 OHLCV,却经历截然不同的盘中波动。
在解读这些字段之前,先弄清它们属于什么对象。
区分股票、指数、基金与 ETF
股票:一家公司的部分所有权
股票(Stock / Equity)是证券的一种,持有股票通常代表持有一家公司的部分所有权。股票价格是市场参与者对这份所有权进行买卖形成的价格。美国证券交易委员会面向投资者的网站也将股票概括为给予持有人公司所有权份额的证券,参见 Investor.gov 的股票说明。
持有一只股票,风险通常集中在一家公司。公司经营、行业变化、治理问题和突发事件都可能直接影响价格。
指数:一把衡量市场的尺子
指数(Index)按照预先规定的方法,把一篮子证券的表现汇总成一个数值。它可以代表整个市场、某个规模区间或某个行业。
指数本身更像一把尺子,不是装着资产的账户,通常不能直接买入。Investor.gov 对市场指数的说明同样强调:指数衡量一篮子证券的表现,投资者不能直接投资指数,只能通过跟踪指数的产品间接获得敞口,参见 Index Funds。
因此,“指数上涨 1%”和“某只指数基金上涨 1%”不是同一个事实。基金还会受到费用、交易成本、跟踪方法和跟踪误差影响。
基金:把多位投资者的资金放进一个组合
基金(Fund)把投资者的资金汇集起来,按照约定的目标和规则持有股票、债券或其他资产。投资者持有的是基金份额,而不是直接分别持有组合中的每一项资产。
基金只是一个大类。不同基金在法律结构、申购赎回、估值、交易方式和费用上可能非常不同,不能仅凭“基金”二字推断具体规则。
ETF:可以在交易所交易的基金
ETF(Exchange-Traded Fund,交易型开放式指数基金,也常称交易所交易基金)既是基金,又能在证券交易所买卖。上交所将 ETF 解释为跟踪标的指数、在交易所上市交易的基金;投资者通过一种产品即可交易一篮子证券,参见上交所 ETF 基础知识。
本教程第一阶段所说的 ETF,特指跟踪宽基指数的普通 ETF,不包括杠杆、反向、单一股票、商品或其他结构更复杂的产品。
宽基指数(Broad-market Index)覆盖较广的一组证券,不只押注一家企业或一个很窄的主题。宽基 ETF 仍然会下跌,也存在流动性、跟踪误差、费用和市场风险;“一篮子资产”不等于“没有风险”。
| 对象 | 它是什么 | 能否像证券一样直接交易 | 主要价格或数值 |
|---|---|---|---|
| 股票 | 一家公司的所有权份额 | 通常可以 | 每股交易价格 |
| 指数 | 一篮子证券表现的统计指标 | 通常不可以 | 指数点位 |
| 基金 | 按规则管理的一组资产 | 取决于基金结构 | 基金份额净值或交易价格 |
| ETF | 在交易所交易的基金 | 可以 | 每份交易价格,同时有基金净值 |
表格不能代替产品规则
这里的“通常”和“取决于”非常重要。产品规则会随市场和品种变化,真实交易前必须阅读交易所规则与基金法律文件,不能把教学表格当作交易依据。
准确标识一个交易对象
代码里的 symbol 是证券代码(Ticker / Symbol)。它是市场用来识别证券的短标识,但证券代码通常不是全球唯一主键。
例如,同一串字符可能在不同交易所代表不同对象;同一产品也可能拥有不同币种或不同交易市场的份额。研究数据至少应同时记录:
market:市场或交易所;symbol:证券代码;currency:价格使用的货币;asset_type:股票、ETF、指数或其他类型。
单元 00 的 CSV 为了保持最小,只保存了 symbol。它的其他信息记录在数据元文件中:
| 属性 | DEMO_ETF 的口径 |
|---|---|
| 市场 | 虚构的 DEMO 市场 |
| 证券代码 | DEMO_ETF |
| 资产类型 | 虚构宽基 ETF 教学标的 |
| 货币 | 人民币(CNY) |
| 时区 | Asia/Shanghai |
DEMO_ETF 不对应真实证券。这里使用人民币和 Asia/Shanghai,只是为了让单位与时间口径完整,并不表示它遵守中国内地市场的撮合、涨跌停、交收或交易费用规则。
读懂 OHLCV
OHLCV 是五个英文字段的缩写:Open、High、Low、Close、Volume。它们把一个时间区间内的成交压缩成一条柱状行情(Bar)。本单元的区间是一个交易日,所以称为日线(Daily Bar)。
| 字段 | 中文 | 本数据单位 | 含义 |
|---|---|---|---|
open | 开盘价 | CNY/份 | 该交易日的正式开盘价格 |
high | 最高价 | CNY/份 | 该交易日内的最高成交价格 |
low | 最低价 | CNY/份 | 该交易日内的最低成交价格 |
close | 收盘价 | CNY/份 | 该交易日的正式收盘价格 |
volume | 成交量 | 份 | 该交易日累计成交的份额数量 |
“开盘价是第一笔成交、收盘价是最后一笔成交”只是常见直觉,不是跨市场通用定义。交易所可能使用开盘或收盘集合竞价、成交量加权平均等规则确定正式价格,而且规则会修订。例如,深交所已发布自 2026 年 7 月 6 日起施行的《深圳证券交易所交易规则(2026 年修订)》。使用真实数据时,应查询对应日期、市场和品种当时有效的规则。
OHLC 的基本关系
在正常日线中,最高价不能低于开盘价或收盘价,最低价不能高于开盘价或收盘价:
high ≥ max(open, close)
low ≤ min(open, close)
high ≥ low对于 2024-01-02:
最高价 101.20 ≥ 开盘价 100.00 和收盘价 100.80
最低价 99.80 ≤ 开盘价 100.00 和收盘价 100.80这些关系可以帮助发现明显错误,但通过检查不代表数据一定正确。把两个仍然满足关系的错误数字对调,程序可能完全看不出来。数据验证需要结构检查、业务规则、其他数据源对照和人工观察共同完成。
成交量不是成交额
成交量(Volume)回答“成交了多少股或多少份”;成交额(Turnover / Amount)回答“这些成交总共涉及多少货币金额”。二者单位不同。
不能简单使用 收盘价 × 成交量 得到精确成交额,因为一天中的每笔成交价格并不都等于收盘价。只有逐笔累加“成交价格 × 成交数量”,才能得到准确成交额;数据供应商也可能直接提供这个字段。
运行行情检查程序
执行:
uv run python examples/unit01/inspect_ohlcv.py预期输出:
OHLCV 检查通过
记录数: 10 个交易日
自然日跨度: 14 天
首日行情: 2024-01-02, 开 100.00, 高 101.20, 低 99.80, 收 100.80, 量 1000000
图表已保存: outputs/unit01/demo_etf_ohlcv.png程序完成三件事:
- 检查必要字段、日期顺序、重复日期和正数值;
- 检查每一行的 OHLC 基本关系;
- 生成价格与成交量图。
程序如何检查 OHLC
打开 examples/unit01/inspect_ohlcv.py,找到下面的逻辑:
highest_open_or_close = prices[["open", "close"]].max(axis=1)
lowest_open_or_close = prices[["open", "close"]].min(axis=1)
valid_ohlc = (
(prices["high"] >= highest_open_or_close)
& (prices["low"] <= lowest_open_or_close)
& (prices["high"] >= prices["low"])
)axis=1 表示在每一行内比较。程序先计算这一行中最高价至少应该达到的上界,以及最低价至多应该达到的下界,然后得到一列布尔值。只要有一行是 False,程序就报告对应日期并停止。
这段实现稍微冗长,是为了把业务规则直接写进代码。可读的规则比一个难以解释的巧妙表达式更适合研究代码。
程序如何画图
价格图同时画出三类信息:
- 灰色竖线:当天最低价到最高价的完整区间;
- 蓝色三角:开盘价;
- 红色圆点和连线:收盘价及其跨交易日变化;
- 下方柱形:每日成交量。
图中 2024-01-05 到 2024-01-08、2024-01-12 到 2024-01-15 之间的空档不是自动判定出的“数据缺失”,只是自然日期轴上没有记录。是否缺失,要结合市场的正式交易日历判断。
区分自然日、交易日与数据缺失
自然日(Calendar Day)是日历上的每一天。交易日(Trading Day)是某个市场按规则开放交易的日期。周末通常不是交易日,节假日、临时休市和特殊安排也会影响交易日历。
样例从 2024-01-02 到 2024-01-15,首尾都计入时跨越 14 个自然日,但只有 10 条日线记录。这里缺少的 4 天是人为按周末留出的间隔,不是数据丢失。
然而,不能看到周六、周日就断言任何市场都休市,也不能看到工作日没有数据就断言文件损坏。加密资产、外汇、期货和不同国家证券市场拥有不同交易时间。真实研究必须使用与市场、品种和历史时期匹配的交易日历。单元 02 会把“预期没有记录”和“本应有记录却缺失”分开检查。
日线的 date 通常表示这条行情所属的交易日,而不是一条精确到秒的成交时间。跨时区研究时,仅有日期可能不够,还需要记录时区、交易时段和数据截止时间。
理解复权
一句话理解复权
复权不是修改真实历史,也不会让账户里多出钱。它只是把不同日期的价格换算到同一套刻度上,方便比较。
为什么历史价格会出现“刻度不同”?股票和 ETF 可能发生拆分、合并、现金分红等公司行动(Corporate Action)。公司行动可能同时改变每股价格、持有数量或收到的现金。只盯着价格一列,就可能把一次机械调整误认为真正的涨跌。
先看最直观的例子:1 股变成 2 股
假设你持有 1 股,拆分前每股价格为 100 元。现在发生“1 股拆成 2 股”:
| 时点 | 持有数量 | 每股价格 | 持仓价值 |
|---|---|---|---|
| 拆分前 | 1 股 | 100 元 | 1 × 100 = 100 元 |
| 拆分后 | 2 股 | 50 元 | 2 × 50 = 100 元 |
价格从 100 元变成 50 元,看起来跌了 50%;但你的数量从 1 股变成 2 股,总价值仍是 100 元。这里为了讲清机制,暂时忽略拆分前后的市场波动。Investor.gov 对股票拆分也给出了同样的直觉:拆分增加股份数量,但不会因此改变股东权益,例如 100 股、每股 100 美元可以变成 200 股、每股 50 美元,参见 Stock Split。
如果直接把两个真实报价连起来,原始价格图会是:
拆分前一天:100 元
拆分后一天: 50 元
图表看起来:下跌 50%
实际情况:股数翻倍,单纯由拆分造成的财富变化为 0这条 50% 的断崖会污染收益率、均线和波动率。复权要做的,就是消除这类由计价单位变化造成的假跳跃。
再看现金分红:一部分价值变成现金
假设除息前每份价格为 100 元,每份派发 2 元现金。为了只观察分红机制,假设没有税费和其他市场波动,除息后的理论价格约为 98 元:
分红前:持有价值 100 元
分红后:证券价格 98 元 + 收到现金 2 元 ≈ 100 元只看价格,会得到 98 / 100 - 1 = -2%;把收到的现金也算进去,经济价值近似没有变化。现实中的除息日价格仍会受市场买卖影响,不保证恰好下降 2 元,但“部分价值从证券价格转成现金”的直觉不变。谁有权收到分红取决于除息日等规则,参见 Investor.gov 的除息日说明。
复权究竟改了什么
复权(Price Adjustment)只改研究数据中的价格表示,不改真实发生过的事情:
- 真实成交仍然发生在当时的不复权价格;
- 拆分后账户中的持有数量确实会改变;
- 分红现金是否到账由真实公司行动决定;
- 复权后的历史价格可能从未在市场上成交过。
可以把它理解成单位换算。一个房间长度分别写成“3 米”和“300 厘米”,数字不同但长度相同;要画连续曲线,就先把它们换成同一种单位。复权是在统一价格序列的比较尺度。
用拆分例子理解前复权和后复权
原始报价是 100 → 50,我们希望把它改写成一条没有假跌幅的序列。可以选择两种基准:
不复权:100 → 50 保留两个日期当时的真实报价
前复权: 50 → 50 保留现在的 50,把拆分前的 100 换算成当前尺度
后复权:100 → 100 保留最初的 100,把拆分后的 50 换算成起点尺度因此可以这样记:
- 前复权通常保留最新价格,调整前面的历史价格;
- 后复权通常保留最早价格,调整后面的价格;
- 两者只是选择了不同的基准点,不应该凭空产生不同的经济收益。
| 口径 | 你看到的是什么 | 更适合回答什么 | 不能直接拿来做什么 |
|---|---|---|---|
| 不复权 | 当时市场上的名义报价 | 当天实际价格、能否成交、涨跌幅限制 | 直接跨公司行动计算长期收益 |
| 前复权 | 以较新价格为基准的连续历史曲线 | 观察走势、计算连续指标 | 把调整后的历史价当作真实成交价 |
| 后复权 | 以较早价格为基准的连续曲线 | 从起点观察累计变化 | 把后期换算价当作真实成交价 |
到底应该使用哪一种
先看问题,不要先选一个“最好的”口径:
- 想知道某天市场真实报价是多少,使用不复权价格;
- 想画一条不被拆分打断的走势或计算连续指标,使用复权价格;
- 想计算包含现金分红的持有收益,需要能够反映分红和再投资假设的总回报序列,不能只看普通收盘价;
- 想模拟历史成交,成交价格应来自当时可交易的不复权价格,同时另行处理拆分、分红和持仓数量变化。
“前复权”不是完整的数据口径
不同数据供应商对现金分红、税费、再投资、复权因子和基准日期的定义可能不同。使用前至少要确认:调整了哪些公司行动、保留哪个基准日、分红是否再投资、收益计算和成交模拟分别使用哪一列。
当前样例没有复权含义
本单元的 DEMO_ETF 是手工合成数据,没有拆分、分红或其他公司行动。因此它并非“不复权价格”,更准确的说法是:这份数据没有复权含义。
完成行情口径说明
一份行情不能只写“ETF 日线”。至少应说明标的、市场、货币、时间和价格口径。对当前样例,我们可以给出完整描述:
数据为虚构
DEMO市场中的合成宽基 ETF 教学标的DEMO_ETF,货币单位为 CNY,时区口径为 Asia/Shanghai。文件包含 2024-01-02 至 2024-01-15 的 10 条日频 OHLCV,按交易日期升序排列。价格单位为 CNY/份,成交量单位为份。数据由项目手工构造,不对应真实证券,不包含公司行动,因此没有复权含义,也不能用于投资结论。
回测前先写清数据口径
以后每次拿到新数据,都应能写出类似的一段话。如果无法说明其中某一项,说明你还不够了解这份数据,不应急着回测。
为什么从日频宽基 ETF 开始
这是教学选择,不是收益判断。
- 日频数据比分钟或逐笔数据更容易观察,暂时不需要处理复杂的盘中撮合;
- 宽基 ETF 比单只股票少一层公司选择问题;
- 单一标的便于先走通数据、信号、持仓、成本和评价的完整链路;
- ETF 有可交易价格,可以在后续明确讨论成交与成本;
- 同一个标的可以作为买入持有基准,减少初学阶段的对象切换。
这些优点不意味着回测自动可信。ETF 仍可能有分红、停牌、折溢价、流动性不足和跟踪误差;日线也无法告诉我们盘中是否真的能按假设价格成交。后续单元会逐项加入这些现实约束。
常见陷阱
把证券代码当成全球唯一标识
只保存 symbol 容易把不同市场的同名代码混在一起。至少同时保存市场、货币和资产类型。
把指数当成可直接成交的资产
指数是统计指标。回测指数与实际交易跟踪它的 ETF,费用、跟踪误差、交易时间和可成交价格都可能不同。
把收盘价当成随时能成交的价格
收盘价是一个特定时点或特定规则产生的行情字段。策略在收盘后才得到信号,就不能再假设自己已经用同一个收盘价成交。这个时间问题会在单元 04 和 05 正式处理。
把成交量当成成交额
数量与货币金额不是同一单位,close × volume 也不是精确成交额。
把没有记录的日期都填成零
非交易日没有行情,不代表价格和成交量都等于零。随意补零会制造不存在的价格暴跌。
混用复权口径
不复权、前复权和后复权序列不能不加说明地拼接。下载数据时必须保存供应商、字段定义和复权方法。
从合成数据得出市场结论
DEMO_ETF 的走势是人为安排的,只能验证代码与概念。它不能证明趋势存在,也不能证明任何策略有效。
验证结果
再次执行:
uv sync --locked
uv run python examples/unit01/inspect_ohlcv.py完成标准:
- 终端明确显示
OHLCV 检查通过; - 记录数是 10 个交易日,自然日跨度是 14 天;
- 首日 OHLCV 与 CSV 第一行一致;
outputs/unit01/demo_etf_ohlcv.png成功生成;- 你能用自己的话解释图中灰线、蓝色三角、红色圆点和柱形分别代表什么;
- 你能完整复述当前样例的市场、货币、时间和复权口径。
图表只是检查证据的一部分:程序规则和终端摘要验证结构,数据元文件说明来源,人工读图帮助发现明显异常;它们不能互相替代。
小结
本单元把一行 CSV 还原成了它背后的市场含义:
市场 + 证券代码 + 货币
↓
某个交易日
↓
open / high / low / close / volume
↓
价格区间、收盘轨迹与成交量图
↓
明确的数据来源和复权口径我们现在能读懂数据,但还没有系统检查重复、缺失和异常。下一单元进入获取并检查行情数据。
练习
基础练习:读懂一天
从 CSV 中选择 2024-01-09,不运行任何新计算,回答:
- 当天开盘价、最高价、最低价、收盘价和成交量分别是多少?
- 收盘价高于还是低于开盘价?
- 当天完整价格区间是多少?
- 你的答案分别使用什么单位?
然后在生成的图中找到同一天,确认视觉位置与数字一致。
进阶练习:让错误被程序发现
复制 CSV 到临时文件,把某一天的 high 改成低于 close 的数值,再让 load_prices 读取这个临时文件。观察错误信息,然后删除临时文件。保留原始样例不变,以免影响后续练习。
思考:还有哪些错误仍然满足 OHLC 基本关系,因此无法被当前程序发现?
延伸思考:选择价格口径
假设你有两个任务:
- 计算一只长期分红 ETF 的历史持有收益;
- 模拟某个历史交易日提交的限价单能否成交。
分别应该优先研究复权价格还是不复权价格?还需要哪些公司行动或成交信息?先写出理由,不需要编程。