Python股票自动交易系统源码解析:从回测到实盘的工程化实践

简介:这份资源是面向高校计算机、金融相关专业毕业设计场景的完整项目源码包,主题为基于Python的股票自动交易系统,适合需要完成毕设选题、课程设计或自学金融量化开发的学生与开发者参考。压缩包共687个文件,整体约14.93MB,涵盖Java后端源码、JavaScript与HTML/CSS前端页面、XML配置、数据库文件及Dockerfile、YML等部署配置,另含大量jpg、png界面截图与图标字体资源,结构较为完整。项目源码经过本地编译验证,按文档配置环境后即可运行,难度适中,内容经助教老师审定,能够满足学习与使用需求。目前已有578人学习下载,读者可从中获取完整的系统实现方案、前后端模块划分思路、界面样式组织方式以及环境搭建与排错参考,便于快速理解股票自动交易系统的整体架构并在此基础上进行二次开发或论文撰写。

1. 从一份股票自动交易系统源码说起:它到底能跑通什么

很多做量化方向毕业设计的同学,卡点从来不是「策略不会写」,而是「整套系统跑不起来」。行情接不进来、订单发不出去、回测和实盘两套代码、数据库一重启数据全丢——这些才是真正让人熬夜的东西。这份基于 Python 的股票自动交易系统源码,解决的正是「从零到一套能演示、能答辩、能继续改」的完整链路问题。它把行情获取、策略信号、下单模拟、持仓管理、回测统计这几块串成了一个可运行的整体,而不是丢给你一堆散装脚本。适合谁?一是毕设选题落在金融商贸或 Python 工程方向、需要一套结构完整可讲解系统的同学;二是已经会写单文件策略、但没搭过工程化交易框架的开发者。它不承诺帮你赚钱,承诺的是让你看懂一套交易系统该有哪些模块、数据怎么流、状态怎么存。

2. 拆开这套系统:模块划分与数据流走向

拿到源码包,第一件事不是急着python main.py,而是先把目录结构和数据流摸清楚。一套合格的自动交易系统,本质上是「数据源 → 策略引擎 → 风控 → 执行 → 持久化」这条流水线,任何一环缺失,系统都只能算玩具。

2.1 典型目录结构与各模块职责

这类项目的常见组织方式是按职责分层,我一般会先确认下面这几层是否齐全:

目录/文件职责缺失后果
data/行情拉取、本地缓存、数据清洗回测无数据可用,只能硬编码
strategy/策略基类与具体策略实现无法扩展新策略,只能改主逻辑
engine/回测引擎、实盘调度循环回测与实盘两套代码,维护地狱
broker/下单接口封装(模拟/实盘)换券商要重写全部交易逻辑
risk/仓位、止损、单笔限额一次异常信号可能打光全部资金
db/持仓、订单、成交记录持久化程序一挂,状态全丢
config.py参数集中管理参数散落各处,改一个漏三个

如果源码里strategy/和engine/是耦合在一起的,说明它更偏「演示脚本」而非「工程框架」,这时候你要有心理准备:想加第二个策略,得动主循环。

2.2 数据从行情到订单的完整链路

理解数据流,比读任何注释都管用。典型链路是这样的:

# 伪代码,展示数据流走向,非可直接运行 market_data = fetch_realtime(symbol) # 1. 拉取行情 signal = strategy.on_bar(market_data) # 2. 策略产出信号 order = risk.check(signal, position) # 3. 风控过滤 if order: result = broker.send_order(order) # 4. 执行下单 db.save_order(result) # 5. 落库 position.update(result) # 6. 更新持仓

这段流程里,每一步都有讲究。fetch_realtime返回的数据结构决定了后面所有模块的字段访问方式,常见做法是统一成 dict 或 DataFrame,字段名固定为open/high/low/close/volume/datetime。strategy.on_bar是策略的唯一入口,好的设计会让它只负责「判断」,不负责「下单」,这样回测和实盘才能共用同一份策略代码。risk.check是最容易被毕设忽略的一环,但它恰恰是答辩时最能体现工程思维的模块——老师一问「如果信号连续触发怎么办」,有风控的能答,没有的只能尴尬。

broker.send_order这一层,模拟盘和实盘的区别通常只体现在这个类的实现上。源码里如果提供了SimulatedBroker和某个实盘接口的抽象基类,说明作者是懂解耦的。db.save_order建议用 SQLite 起步,零配置、单文件、方便打包,答辩演示时不用现场装数据库。

2.3 回测引擎与实盘循环的复用关系

新手最容易翻车的地方,是把回测写成「读历史 CSV 循环算收益」,把实盘写成「while True 拉行情下单」,两套逻辑各写各的。结果就是回测赚钱、实盘亏钱,还找不到原因。

正确的做法是抽出一个Engine,它接收一个「数据迭代器」和一个「策略实例」,回测时喂历史数据,实盘时喂实时数据:

class Engine: def __init__(self, data_feed, strategy, broker, risk): self.data_feed = data_feed # 回测=历史迭代器,实盘=实时队列 self.strategy = strategy self.broker = broker self.risk = risk def run(self): for bar in self.data_feed: signal = self.strategy.on_bar(bar) order = self.risk.check(signal, self.broker.position) if order: self.broker.send_order(order)

这样策略代码只写一遍,回测和实盘的行为差异被压缩到data_feed和broker两个可替换组件里。参数上,回测模式把data_feed设成历史数据生成器,实盘模式设成实时行情订阅;broker回测时用模拟撮合、实盘时用真实接口。这个设计值不值得抄?非常值得,它是区分「课程作业」和「像样系统」的分水岭。

3. 把系统跑起来:环境、配置与第一次回测

摸清结构之后,才轮到动手。这一章的目标很明确:让源码在你机器上跑出第一份回测报告。别小看这一步,环境依赖和配置项是新手翻车最密集的区域。

3.1 环境依赖与虚拟环境搭建

Python 量化项目的依赖通常集中在pandas、numpy、matplotlib,行情接口可能用到akshare、tushare或baostock这类库。先建虚拟环境,别直接往全局环境装:

python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install -r requirements.txt

如果源码没带requirements.txt,按报错逐个装也行,但更稳的做法是先看import语句统计一遍。常见坑是pandas版本差异导致append方法报错(2.0 之后被移除),这时候要么降版本,要么把代码里的df.append改成pd.concat。参数上,建议 Python 用 3.8~3.10,太新的版本有些老库还没适配。

3.2 配置文件里必须改的几个参数

config.py或config.yaml是系统的总控台,第一次运行前,下面这几项必须确认:

# config.py 示例 SYMBOL = "000001" # 标的代码,先拿单只股票跑通 START_DATE = "2022-01-01" # 回测起始 END_DATE = "2023-12-31" # 回测结束 INIT_CASH = 100000 # 初始资金 COMMISSION = 0.0003 # 手续费率,别设 0,否则回测虚高 SLIPPAGE = 0.001 # 滑点,模拟真实成交偏差 STRATEGY = "ma_cross" # 策略名,对应 strategy/ 下的类

COMMISSION和SLIPPAGE是最容易被设成 0 的两个参数,设 0 的回测结果会明显偏乐观,答辩时被问到「你考虑交易成本了吗」就露馅了。INIT_CASH建议设成真实可承受的量级,别动辄一个亿,没有意义。SYMBOL第一次跑一定用单只、流动性好的标的,跑通再扩到多只。

3.3 跑通第一次回测并读懂输出

配置改完,执行入口脚本:

python main.py --mode backtest

跑完之后,重点看三样东西:总收益率、最大回撤、交易次数。总收益率高但交易次数只有两三次的,基本是运气,别高兴太早;最大回撤超过 30% 的,说明风控形同虚设;交易次数几百次但收益为负的,多半是手续费和滑点吃掉了利润。

如果输出里有资金曲线图,重点看曲线是不是「台阶式」的——健康的策略曲线应该是缓慢爬升伴随回撤,而不是一根直线冲上去再断崖式下跌。后者往往是过拟合或者未来函数(用了当天收盘价决定当天开盘操作)的典型特征。这一步跑通,你才算真正拥有了这套系统。

4. 策略与风控:从均线交叉到仓位管理

系统能跑之后,核心问题变成「策略怎么写、风控怎么加」。这也是毕设答辩时老师最爱追问的部分,因为它直接体现你对交易逻辑的理解深度。

4.1 写一个可复用的均线交叉策略

均线交叉是最经典的入门策略,但写法上有讲究。关键是策略类要继承统一的基类,只实现on_bar,不碰下单逻辑:

class MaCrossStrategy(BaseStrategy): def __init__(self, short=5, long=20): self.short = short # 短周期 self.long = long # 长周期 self.prices = [] # 缓存收盘价 def on_bar(self, bar): self.prices.append(bar["close"]) if len(self.prices) < self.long: return None # 数据不够,不产生信号 ma_short = sum(self.prices[-self.short:]) / self.short ma_long = sum(self.prices[-self.long:]) / self.long if ma_short > ma_long: return {"action": "buy", "reason": "golden_cross"} elif ma_short < ma_long: return {"action": "sell", "reason": "death_cross"} return None

short和long是最核心的两个参数,常见组合是 5/20、10/30、5/60。周期越短信号越频繁、手续费越高;周期越长信号越少、越滞后。self.prices缓存全部历史价格在数据量大时会吃内存,工程上更稳的做法是只保留最近long个,或者用collections.deque(maxlen=self.long)。返回的 dict 里带reason字段是个好习惯,方便回测时统计每类信号的胜率。

4.2 风控模块:仓位、止损与单笔限额

策略只管「想不想买」,风控管「能买多少、什么时候必须卖」。一个最小可用的风控模块至少包含三层:

class RiskManager: def __init__(self, max_position=0.3, stop_loss=0.05, max_orders=10): self.max_position = max_position # 单标的最大仓位占比 self.stop_loss = stop_loss # 止损线 self.max_orders = max_orders # 单日最大下单次数 def check(self, signal, position, cash): if signal is None: return None if self._daily_orders >= self.max_orders: return None # 超过单日限额,拒绝 if signal["action"] == "buy": amount = cash * self.max_position return {"action": "buy", "amount": amount} if signal["action"] == "sell": return {"action": "sell", "amount": position} return None

max_position=0.3意味着单只股票最多用三成资金,防止一次错误信号打光弹药。stop_loss=0.05是 5% 止损,触发后强制平仓。max_orders防的是策略在震荡行情里反复买卖、被手续费磨死。这三个参数没有标准答案,但答辩时你能说清「为什么设这个值」,比参数本身是多少更重要。

4.3 回测指标怎么读才算没白跑

回测跑完,除了收益率,下面几个指标必须会看:

指标含义健康区间参考
最大回撤从峰值到谷底的最大亏损一般 < 20%
夏普比率单位风险的超额收益> 1 算不错
胜率盈利交易占比40%~60% 常见
盈亏比平均盈利/平均亏损> 1.5 较健康
交易次数总成交笔数太少无统计意义

单看收益率是外行做法。一个收益率 50% 但最大回撤 60% 的策略,实盘时你根本拿不住。夏普比率和盈亏比结合起来看,才能判断策略是「靠运气」还是「靠逻辑」。如果源码的回测模块没输出这些指标,自己加几行统计代码就行,这是加分项。

5. 避坑与排查:那些让系统跑不起来的常见问题

这一章是我踩过的坑合集,每一条都对应一个具体现象。新手照着排查,能省下大量瞎试的时间。

5.1 行情接口拉不到数据

现象:运行后报连接超时或返回空 DataFrame。原因:行情接口通常有频率限制,或者需要先注册获取 token,也可能是标的代码格式不对(有的接口要000001.SZ,有的只要000001)。解决:先单独写个最小脚本测试接口,确认能拿到数据再接入系统;加time.sleep控制请求频率;核对代码格式与接口文档一致。

5.2 回测结果好得离谱

现象:收益率几百个点,资金曲线几乎直线上升。原因:典型的未来函数——用了当天收盘价决定当天操作,或者手续费、滑点设成了 0。解决:检查策略里是否用了bar["close"]去决定同一根 bar 的买卖;把COMMISSION和SLIPPAGE设成真实值;用样本外数据再验证一遍。

5.3 程序跑一段时间内存暴涨

现象:回测跑几万根 bar 后内存占用持续上升,最后卡死。原因:策略里用 list 无限缓存历史价格,或者数据库连接没关闭。解决:用deque(maxlen=N)限制缓存长度;数据库操作统一用上下文管理器with自动关闭连接。

5.4 实盘和回测结果对不上

现象:同一策略回测赚钱,模拟盘亏钱。原因:回测用收盘价成交,实盘用实时价成交,两者存在时间差;或者回测没考虑涨跌停无法成交的情况。解决:回测时用下一根 bar 的开盘价成交,更贴近现实;在broker层加涨跌停判断,无法成交就跳过。

5.5 数据库文件越跑越大

现象:db文件从几 MB 涨到几百 MB。原因:每根 bar 都写库,或者订单记录从不清理。解决:只持久化订单和成交,行情数据用文件缓存;定期归档历史记录,或者按日期分表。

6. 进阶玩法:把单策略系统改成多策略调度

单策略跑通之后,真正有意思的是让它同时跑多个策略、共享资金池。这一步能把你的毕设从「能演示」拉到「有架构」。核心思路是引入一个调度层,每个策略独立产出信号,由统一的资金分配器决定谁优先成交。

class MultiStrategyEngine: def __init__(self, strategies, allocator, broker, risk): self.strategies = strategies # 策略列表 self.allocator = allocator # 资金分配器 self.broker = broker self.risk = risk def on_bar(self, bar): signals = [] for s in self.strategies: sig = s.on_bar(bar) if sig: signals.append((s.name, sig)) # 按信号强度或策略权重排序,资金有限时优先满足高分信号 signals.sort(key=lambda x: x[1].get("score", 0), reverse=True) for name, sig in signals: budget = self.allocator.allocate(name) order = self.risk.check(sig, budget) if order: self.broker.send_order(order)

allocator是关键,常见做法有等权分配(每个策略固定比例)、动态分配(按近期表现调整权重)、风险平价(按波动率倒数分配)。等权最简单,适合毕设;动态分配更接近实盘,但容易过拟合。signals.sort里的score字段需要策略自己给出,可以是信号强度、历史胜率或任意打分逻辑。

多策略带来的新问题是「资金冲突」——两个策略同时想买但钱不够。allocator的职责就是提前把资金切分好,避免运行时争抢。另一个坑是策略之间的持仓互相干扰,建议每个策略维护独立的虚拟持仓,最终由 broker 层合并成真实持仓。

验证多策略是否真的有效,别只看总收益,要看「策略相关性」——如果两个策略的信号高度同步,那加在一起等于加杠杆,没有分散风险的作用。可以统计两个策略每日收益的相关系数,低于 0.5 才算真正互补。

从那以后我每次拿到一套交易系统源码,都强制先跑一遍最小回测、再手动改一个参数看结果变化,确认自己真的理解了数据流,而不是只会按回车。希望这套拆解能帮到你,把这份源码真正变成你自己的东西。

本文还有配套的精品资源,点击获取

相关新闻

AI时代程序员转型指南:从会写代码到驾驭AI做工程决策

AI时代程序员转型指南:从会写代码到驾驭AI做工程决策

1. 别被标题吓到&#xff1a;AI首先替代的是不会用AI的程序员&#xff0c;而不是所有程序员1.1 从“裁员焦虑”到“招聘变化”&#xff1a;我看到的真实信号大模型时代喊了两年多&#xff0c;身边不少写代码的朋友都在问&#xff1a;AI到底会不会抢我饭碗&#xff1f;包括我自己…

2026/10/20 23:29:04 阅读更多 →
Agent工程栈五层拆解:任务编排、工具调用、记忆管理、可观测性与多Agent协作

Agent工程栈五层拆解:任务编排、工具调用、记忆管理、可观测性与多Agent协作

1. 从五条动态里拆出来的工程信号10 月 7 日那天&#xff0c;我关注的几个技术社区里集中冒出来五条动态&#xff0c;主题各不相同&#xff0c;但放在一起看&#xff0c;指向的是同一件事&#xff1a;Agent 这套东西&#xff0c;正在从"演示能跑"往"工程能交付&…

2026/10/20 23:28:02 阅读更多 →
PPT认知工程:信息压缩、视觉锚点与逻辑呼吸感

PPT认知工程:信息压缩、视觉锚点与逻辑呼吸感

1. “PPT-Master”不是软件名&#xff0c;而是一类人的隐性职业标签最近在几个技术协作群和高校教学支持论坛里&#xff0c;频繁看到有人用“PPT-Master”自称或称呼他人——不是指某款叫这个名字的工具&#xff0c;也不是某个开源项目&#xff0c;而是一种带着自嘲又带点敬意的…

2026/10/20 23:28:02 阅读更多 →

最新新闻

博科48000交换机安装维护实战指南:端口规划与Zoning配置

博科48000交换机安装维护实战指南:端口规划与Zoning配置

简介&#xff1a;本资源是博科&#xff08;Brocade&#xff09;光纤通道SAN交换机的官方级安装与维护实操手册&#xff0c;面向企业IT运维工程师、存储网络架构师及SAN系统实施人员&#xff0c;聚焦FC-SAN环境下的设备部署、冗余规划、微码升级与日常维护等核心运维场景。文档共…

2026/10/21 0:10:20 阅读更多 →
Java对接支付宝对账单下载接口:从调用到解析入库的完整实践

Java对接支付宝对账单下载接口:从调用到解析入库的完整实践

1. 对账单下载这件事&#xff0c;为什么值得单独拎出来讲做过支付对接的人都有一个共识&#xff1a;支付主链路&#xff08;下单、支付、回调&#xff09;反而是最简单的部分&#xff0c;真正让人头疼的是对账。下单支付是实时链路&#xff0c;出了问题用户马上就能反馈&#x…

2026/10/21 0:10:20 阅读更多 →
SpringBoot+Vue+MySQL社区流浪动物救助系统开发全解析

SpringBoot+Vue+MySQL社区流浪动物救助系统开发全解析

简介&#xff1a;这是一套基于Java、SpringBoot、Vue与MySQL实现的社区流浪动物救助系统完整毕业设计项目&#xff0c;面向计算机相关专业学生&#xff0c;适用于毕业设计、课程设计或期末大作业。系统后端采用SpringBoot框架&#xff0c;前端使用Vue.js&#xff0c;数据库脚本…

2026/10/21 0:10:20 阅读更多 →
STM32寄存器白话手册:从硬件开关视角理解位操作

STM32寄存器白话手册:从硬件开关视角理解位操作

1. 为什么“寄存器手册”从来不是一本能直接读完的书&#xff1f;刚接触STM32那会儿&#xff0c;我桌上常年摊着三份文档&#xff1a;ST官方的《Reference Manual》&#xff08;RM&#xff09;、《Datasheet》&#xff08;DS&#xff09;&#xff0c;还有一本被翻得卷了边的《C…

2026/10/21 0:10:20 阅读更多 →
STM32+华为云IoT+Qt智能家居跨平台实战:低功耗、高可靠、真可用

STM32+华为云IoT+Qt智能家居跨平台实战:低功耗、高可靠、真可用

1. 这套系统到底在解决什么真实问题&#xff1f;——从“能连上”到“真好用”的断层很多人第一次听说“STM32华为云IoTQt”这个组合时&#xff0c;第一反应是&#xff1a;又一个技术堆砌的Demo&#xff1f;确实&#xff0c;网上太多项目只停留在“LED灯亮了”“温湿度数据上云…

2026/10/21 0:10:20 阅读更多 →
ADC采样精度与取平均实战:从原理到配置的避坑指南

ADC采样精度与取平均实战:从原理到配置的避坑指南

ADC 这个模块&#xff0c;说起来简单&#xff0c;做起来坑多。很多人第一次接触模数转换&#xff0c;觉得不就是把模拟电压读进来变成数字嘛&#xff0c;配置一下寄存器、开个通道、读个值就完事了。但真正到了项目里&#xff0c;你会发现读回来的数跳得厉害、精度对不上、采样…

2026/10/21 0:09:19 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →