[{"content":"无论是回测还是交易，分析交易系统的表现至关重要：不仅要看是否获利，还要看实现利润的过程中是否承担了过多风险，或者与参考资产（或无风险资产）相比是否真的值得。\n这就是分析器的作用：提供对已发生或当前情况的分析。\n分析器的设计参照了线条对象（例如具有 next 方法），但有一个主要区别：分析器不包含线条。\n这意味着它们不太消耗内存，即使在分析了成千上万个价格柱之后，可能仍然只在内存中保存单个结果。\n在生态系统中的位置 # 分析器（如同策略、观察者和数据）通过 cerebro 实例添加到系统中：\naddanalyzer(ancls, *args, **kwargs) 但在 cerebro.run 期间，对于系统中的每个策略，会发生以下情况：\nancls 会用 *args 和 **kwargs 实例化 实例会附加到策略上 这意味着：\n如果回测运行包含 3 个策略，将创建 3 个 ancls 实例，每个实例附加到不同的策略上。\n结论：分析器分析的是单个策略的表现，而非整个系统。\n附加位置 # 某些分析器可能会使用其他分析器来完成其工作。例如：SharpeRatio 使用 TimeReturn 的输出进行计算。\n这些子分析器也会插入到创建它们的策略中，但对用户是完全不可见的。\n属性 # 分析器提供了一些自动设置的默认属性，方便使用：\nself.strategy：对当前运行策略的引用。策略能访问的任何内容，分析器也能访问。 self.datas[x]：策略中的数据源数组。虽然可以通过策略引用访问，但快捷方式更方便。 self.data：self.datas[0] 的快捷方式。 self.dataX：对应 self.datas[x] 的快捷方式。 还有一些其他别名可用：\nself.dataX_Y，其中 X 对应 self.datas[X]，Y 对应线条，最终指向 self.datas[X].lines[Y]。 如果线条有名称，还可以使用以下方式：\nself.dataX_Name，解析为 self.datas[X].Name，按名称而非索引返回线条。 对于第一个数据，最后两个快捷方式可以省略 X 数字。例如：\nself.data_2 指 self.datas[0].lines[2]。\nself.data_close 指 self.datas[0].close。\n返回分析结果 # Analyzer 基类创建了 self.rets（类型为 collections.OrderedDict）成员属性来返回分析结果。这是在 create_analysis 方法中完成的，子类可以覆盖该方法。\n操作模式 # 虽然分析器不是线条对象，不迭代线条，但它们遵循相同的操作模式。\n在系统启动前实例化（调用 __init__）。 使用 start 信号表示操作开始。 prenext / nextstart / next 方法会根据策略的计算最小周期被调用。 prenext 和 nextstart 的默认行为是调用 next，因为分析器可能从系统启动的第一刻就开始分析。 在线条对象中通常调用 len(self) 检查实际条数。这在分析器中同样适用，返回 self.strategy 的值。 订单和交易通过 notify_order 和 notify_trade 进行通知，与策略相同。 现金和价值通过 notify_cashvalue 方法通知，与策略相同。 现金、价值、基金价值和基金份额通过 notify_fund 方法通知，与策略相同。 使用 stop 信号表示操作结束。 常规操作周期完成后，分析器提供额外的提取/输出方法： get_analysis：通常（非强制）返回包含分析结果的字典样对象。 print：使用标准的 backtrader.WriterFile（除非被覆盖）输出 get_analysis 的结果。 pprint：使用 Python 的 pprint 模块打印 get_analysis 结果。 最后：\nget_analysis 创建一个类型为 collections.OrderedDict 的成员属性 self.ret，分析器将结果写入其中。\n子类可以覆盖此方法以改变行为。\n分析器模式 # 在 backtrader 中开发分析器揭示了两种不同的使用模式：\n通过 notify_xxx 和 next 方法在执行过程中收集信息，并在 next 中生成当前分析。 例如，TradeAnalyzer 仅使用 notify_trade 方法生成统计数据。 按上述方式收集信息（或不收集），但在 stop 方法中进行一次性分析。 SQN（系统质量编号）在 notify_trade 中收集交易信息，但在 stop 方法中生成统计数据。 快速示例 # 尽可能简单：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import datetime import backtrader as bt import backtrader.analyzers as btanalyzers import backtrader.feeds as btfeeds import backtrader.strategies as btstrats cerebro = bt.Cerebro() # 数据 dataname = \u0026#39;../datas/sample/2005-2006-day-001.txt\u0026#39; data = btfeeds.BacktraderCSVData(dataname=dataname) cerebro.adddata(data) # 策略 cerebro.addstrategy(btstrats.SMA_CrossOver) # 分析器 cerebro.addanalyzer(btanalyzers.SharpeRatio, _name=\u0026#39;mysharpe\u0026#39;) thestrats = cerebro.run() thestrat = thestrats[0] print(\u0026#39;Sharpe Ratio:\u0026#39;, thestrat.analyzers.mysharpe.get_analysis()) 执行它（已将其存储在 analyzer-test.py 中）：\n$ ./analyzer-test.py Sharpe Ratio: {\u0026#39;sharperatio\u0026#39;: 11.647332609673256} 没有绘图，因为 SharpeRatio 是计算结束时的单一值。\n分析器解析 # 重申一下，分析器不是线条对象，但为了无缝集成到 backtrader 生态系统中，遵循了线条对象的内部 API 约定（实际上是混合模式）。\n注意\nSharpeRatio 的代码已经演变，例如考虑了年化，此版本仅供参考。\n请查看分析器参考。\n此外，SharpeRatio_A 直接以年化形式返回值，而不受时间范围影响。\nSharpeRatio 的基础代码（简化版本）：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import operator from backtrader.utils.py3 import map from backtrader import Analyzer, TimeFrame from backtrader.mathsupport import average, standarddev from backtrader.analyzers import AnnualReturn class SharpeRatio(Analyzer): params = ((\u0026#39;timeframe\u0026#39;, TimeFrame.Years), (\u0026#39;riskfreerate\u0026#39;, 0.01),) def __init__(self): super(SharpeRatio, self).__init__() self.anret = AnnualReturn() def start(self): # Not needed ... but could be used pass def next(self): # Not needed ... but could be used pass def stop(self): retfree = [self.p.riskfreerate] * len(self.anret.rets) retavg = average(list(map(operator.sub, self.anret.rets, retfree))) retdev = standarddev(self.anret.rets) self.ratio = retavg / retdev def get_analysis(self): return dict(sharperatio=self.ratio) 代码分解说明：\nparams 声明 虽然声明的参数没有使用（作为示例），但分析器像大多数其他对象一样支持参数。 __init__ 方法 就像策略在 __init__ 中声明指标一样，分析器声明辅助对象。 这里：SharpeRatio 使用年度回报计算。计算会自动进行，结果可供 SharpeRatio 使用。 next 方法 SharpeRatio 不需要它，但此方法会在父策略每次调用 next 后被调用。 start 方法 在回测开始前调用，可用于额外的初始化任务。 SharpeRatio 不需要它。\nstop 方法 在回测结束后调用。如 SharpeRatio 所做的那样，可用于完成计算。 get_analysis 方法（返回字典） 为外部调用者提供分析结果。 返回：包含分析结果的字典。\n参考 # class backtrader.Analyzer() Analyzer 基类。所有分析器都是此类的子类。\n分析器实例在策略的框架内运行，为该策略提供分析。\n自动设置的成员属性：\nself.strategy（提供对策略及其可访问内容的访问） self.datas[x] 提供对系统中数据源数组的访问，也可通过策略引用访问 self.data 提供对 self.datas[0] 的访问 self.dataX -\u0026gt; self.datas[X] self.dataX_Y -\u0026gt; self.datas[X].lines[Y] self.dataX_name -\u0026gt; self.datas[X].name self.data_name -\u0026gt; self.datas[0].name self.data_Y -\u0026gt; self.datas[0].lines[Y] 这不是线条对象，但方法和操作遵循相同的设计：\n__init__：实例化和初始设置 start / stop：表示操作的开始和结束 prenext / nextstart / next：方法家族，遵循策略中相同方法的调用 notify_trade / notify_order / notify_cashvalue / notify_fund：接收与策略等效方法相同的通知 操作模式是开放的，没有首选模式。分析可以通过 next 调用生成，在 stop 期间生成，或通过单个方法如 notify_trade 生成。\n关键是覆盖 get_analysis 以返回包含分析结果的字典样对象（实际格式取决于实现）。\nstart() 调用以指示操作开始，给分析器时间设置所需内容。 stop() 调用以指示操作结束，给分析器时间关闭所需内容。 prenext() 在策略的每次 prenext 调用期间调用，直到达到策略的最小周期。 分析器的默认行为是调用 next。 nextstart() 在策略的 nextstart 调用时调用一次，当首次达到最小周期时。 next() 在策略的每次 next 调用期间调用，一旦达到策略的最小周期。 notify_cashvalue(cash, value) 在每个 next 周期之前接收现金/价值通知。 notify_fund(cash, value, fundvalue, shares) 接收当前现金、价值、基金价值和基金份额。 notify_order(order) 在每个 next 周期之前接收订单通知。 notify_trade(trade) 在每个 next 周期之前接收交易通知。 get_analysis() 返回包含分析结果的字典样对象。 字典中分析结果的键和值的格式取决于实现。 甚至不强制结果是字典样对象，只是一种约定。 默认实现返回由默认 create_analysis 方法创建的默认 OrderedDict rets。 create_analysis() 供子类覆盖。提供创建保存分析结果的结构的机会。 默认行为是创建名为 rets 的 OrderedDict。 print(*args, **kwargs) 使用标准 Writerfile 对象打印 get_analysis 返回的结果，默认写入标准输出。 pprint(*args, **kwargs) 使用 Python 的 pprint 模块打印 get_analysis 返回的结果。 len() 支持在分析器上调用 len，实际上返回分析器运行的策略的当前长度。 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/12-analyzers/01-analyzers/","section":"教程","summary":"无论是回测还是交易，分析交易系统的表现至关重要：不仅要看是否获利，还要看实现利润的过程中是否承担了过多风险，或者与参考资产（或无风险资产）相比是否真的值得。\n","title":"Analyzer 分析器详解","type":"docs"},{"content":" Backtrader 是一个基于 Python 实现功能强大且开源的量化回测交易框架。Backtrader 有着完整的基础设施，支持编写可重用组件，如策略、指标和分析器。\nBacktrader 支持多品种多周期多策略的回测。相对于其他专门用于策略回测的框架，Backtrader 不仅仅是可以回测交易策略，还可以连接 Broker 实盘交易。\n有哪些特性呢？ # 稍微展开说说 Backtrader 的特性吧。\n丰富的技术指标\nBacktrader 提供超过 122 种内置指标，包括多种移动平均线（SMA、EMA 等）和经典指标（MACD、随机指标、RSI 等）。\n如果内置指标不能满足需求，Backtrader 还集成了其他技术指标库，如 ta-lib 库。而且，我们也可以自定义技术指标，Backtrader 也提供了自定义指标的能力。\n支持多种数据源\nBacktrader 支持多种数据源类型，如常见的一些数据源：\nCSV 数据文件； Pandas 数据源，可以借助 Pandas 的强大功能连接各种不同的数据源； YahooFinance，这个好像已经不能用了，不过借助 yfinance 包和 pandas 可轻易集成； 其他的一些公开数据源。 除此以外，Backtrader 支持自定义数据格式，如希望实现股票选择策略，可自定义数据格式，加入财务数据和因子，如 PE、PB、ROI 等指标。\nBacktrader 支持同时添加多个 DataFeed 数据源，如多个时间框架数据源实现多周期策略，多标的数据源实现组合交易策略，或是直接进行数据的重采样和重放功能，实现真实交易环境的模拟。\n灵活的策略实现\nBacktrader 支持灵活的策略开发，如在策略初始化阶段，Backtrader 支持预热计算，预热完成才进入交易逻辑部分。\nBacktrader 支持并行运行多个策略，实现多策略组合。其内置的 Broker 支持多种订单类型，如市价单、限价单、止损单等，支持多空交易，自定义佣金方案和信用利息，提供针对期货类工具的连续现金调整，支持基金模式和自定义滑点策略。\n它还提供多种订单生成方法，并支持事件通知机制，包括新数据、订单、交易和计时器等等。\n性能分析与绘图\nBacktrader 内置多个性能分析器，包括时间收益、交易分析器、夏普比率、VWR 和 SQN 等，帮助用户全面评估策略表现。Backtrader 支持通过一行代码绘图（安装 Matplotlib），用户可自定义绘图，评估策略表现。\n架构设计 # Backtrader 本身是一个模块拆分的非常优秀的框架，解耦复用方面做的非常优秀。\n如下所示的一系列组件：\nDataFeed：负责加载行情数据，包括历史数据和实时数据，是策略运行的基础输入。 Cerebro：Backtrader 的核心引擎，用于统一管理数据、策略、分析器、经纪商等组件，并负责执行回测或实盘。 Indicator：用于计算常见的技术指标（如均线、RSI、布林带等），也可以自定义指标逻辑，为策略提供信号依据。 Strategy：定义具体的交易规则和执行逻辑，包括开仓、平仓、止损止盈等，是系统的决策核心。 Broker：模拟或连接真实交易账户，负责订单撮合、资金管理和手续费处理。 Analyzer：对策略的回测结果进行统计与评估，如收益率、最大回撤、夏普比率等。 Observer：用于跟踪策略运行过程并输出可视化结果，如资金曲线、买卖点、回撤变化等。 如下是各个组件之间的关系架构图：\nBacktrader 的核心 Cerebro 调度整个回测流程。\n首先，由 DataFeed 提供行情数据，Cerebro 将数据传递给 Strategy。策略读取数据并调用 Indicator 计算技术信号，据此生成买卖决策并交由 Broker 执行交易。交易完成后，Broker 更新账户与持仓，并将结果反馈给 Cerebro。\n随后，Analyzer 分析策略绩效（如收益、回撤），Observer 记录并可视化资金曲线与交易点。Cerebro 不断循环此流程，直到数据结束，最终汇总输出策略表现。\n整个系统形成从“数据→决策→执行→反馈→分析”的闭环结构。\n实盘交易 # Backtrader 的交易逻辑和经纪商 Broker 的操作是逐事件驱动，这让其可非常容易实现实盘交易。\n我们在回测时，指标计算尽可能通过向量化计算，预加载源数据提升计算速度。而实盘模式下，可在仅事件模式下运行，无需预加载数据。\nBacktrader 支持与多种 Broker 经纪商实时交易，包括 Interactive Brokers、Oanda v1 和 VisualChart。此外，还支持第三方经纪商如 Alpaca、Oanda v2 和 ccxt 的集成。其中，ccxt 是加密货币实盘交易的 Python 库。\n加密货币的程序交易账号相对更加容易获取，后续或许我会用 ccxt 演示 backtrader 的实盘交易加密货币。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/01-intro-install/01-introduction/","section":"教程","summary":" Backtrader 是一个基于 Python 实现功能强大且开源的量化回测交易框架。Backtrader 有着完整的基础设施，支持编写可重用组件，如策略、指标和分析器。\n","title":"Backtrader 简介与核心特性","type":"docs"},{"content":" 类 backtrader.brokers.BackBroker() # Broker 经纪商模拟器，支持不同的订单类型，检查订单现金需求，跟踪每次 Cerebro 迭代的现金和价值，并维护各数据的当前头寸。\n现金在每次迭代中调整，期货等工具的价格变化会导致实际现金增加或减少。 支持的订单类型：\nMarket：将在下一个柱的第一个tick（即开盘价）执行 Close：用于日内交易，订单以会话最后一个柱的收盘价执行 Limit：如果在会话期间看到给定的限价则执行 Stop：如果看到给定的止损价，则执行市场订单 StopLimit：如果看到给定的止损价，则启动限价订单 由于 Broker 由 Cerebro 实例化，用户通常不需要替换 Broker 实例，参数也不直接受用户控制。\n要修改参数，有两种方式：\n手动创建带所需参数的 Broker，通过 cerebro.broker = instance 设置； 使用 cerebro.broker.set_xxx() 方法设置参数。 注意，cerebro.broker 是 Cerebro 的一个属性，由 getbroker 和 setbroker 方法支持。\n参数 # 参数名 默认值 描述 cash 10000 起始现金 commission CommInfoBase(percabs=True) 适用于所有资产的基础佣金方案 checksubmit True 在将订单接受到系统之前检查保证金/现金 eosbar False 对于日内 Bar，考虑与会话结束时间相同的柱为会话结束。通常不会这样，因为许多交易所会在会话结束后几分钟内为许多产品生成一些柱（最终拍卖） filler None 一个可调用对象，签名为 callable(order, price, ago)。参数说明：- order：显然是执行中的订单。这提供了对数据的访问（包括 ohlc 和成交量值）、执行类型、剩余大小（order.executed.remsize）等。- price：订单将在 ago 柱中执行的价格- ago：用于从 order.data 提取 ohlc 和成交量价格的索引。在大多数情况下，这将是 0，但在某些角落情况下，对于 Close 订单，这将是 -1。- 可调用对象必须返回执行的大小（值 \u0026gt;= 0）可调用对象当然可以是一个 __call__ 符合上述签名的对象。默认情况下，订单将一次性完全执行。 slip_perc 0.0 用于买卖订单上下滑动价格的绝对百分比（且为正值）。注意：0.01 是 1%，0.001 是 0.1% slip_fixed 0.0 用于买卖订单上下滑动价格的单位百分比（且为正值）。注意：如果 slip_perc 非零，则优先于此。 slip_open False 是否滑动专门使用下一个柱的开盘价执行的订单价格。例如，市场订单将在下一个可用tick执行，即柱的开盘价。这也适用于其他一些执行，因为逻辑尝试检测开盘价是否会匹配请求的价格/执行类型在移动到新柱时。 slip_match True - 如果为 True，经纪商将通过在高/低价位封顶滑点来提供匹配，以防它们超出。- 如果为 False，经纪商将不会使用当前价格匹配订单，并将在下一次迭代中尝试执行 slip_limit True - 限价订单，给定确切的匹配价格请求，即使 slip_match 为 False，也会被匹配。- 此选项控制该行为。- 如果为 True，那么限价订单将通过在限价/高低价位封顶价格进行匹配- 如果为 False 且滑点超出上限，则不会有匹配 slip_out False 即使价格超出高-低范围，也提供滑点。 coc False Cheat-On-Close 将其设置为 True 与 set_coc 启用，将“市场”订单与订单条的收盘价匹配。这实际上是作弊，因为柱已关闭，任何订单都应首先与下一个柱的价格匹配 coo False Cheat-On-Open 将其设置为 True 与 set_coo 启用，将“市场”订单与开盘价匹配，例如使用设置为 True 的计时器，因为这种计时器在经纪商评估之前执行 int2pnl True 将生成的利息（如果有）分配给减少头寸的操作的利润和亏损（无论是多头还是空头）。在某些情况下，这可能是不希望的，因为不同的策略在竞争，利息将以不确定的方式分配给其中任何一个。 shortcash True 如果为 True，则在卖空类似股票的资产时将增加现金，并且该资产的计算价值将为负值。- 如果为 False，则现金将作为操作成本扣除，计算的价值将为正值，以最终得到相同的金额 fundstartval 100.0 此参数控制基金式绩效测量的起始值，即：现金可以增加和扣除，增加股票数量。绩效不是使用投资组合的净资产价值来衡量，而是使用基金的价值 fundmode False 如果设置为 True，诸如 TimeReturn 的分析器可以基于基金价值而不是总净资产价值自动计算回报 方法 # 签名 描述 set_cash(cash) 设置现金参数（别名：setcash） get_cash() 返回当前现金（别名：getcash） get_value( datas=None, mkt=False, lever=False) 返回给定数据的投资组合价值（如果数据为 None，则返回总投资组合价值）（别名：getvalue） set_eosbar(eosbar) 设置 eosbar 参数（别名：seteosbar） set_checksubmit(checksubmit) 设置 checksubmit 参数 set_filler(filler) 设置用于成交量填充执行的填充器 set_coc(coc) 配置 Cheat-On-Close 方法以在订单柱上买入收盘价 set_coo(coo) 配置 Cheat-On-Open 方法以在订单柱上买入收盘价 set_int2pnl(int2pnl) 配置将利息分配给利润和亏损 set_fundstartval(fundstartval) 设置基金式绩效跟踪器的起始值 set_slippage_perc( perc, slip_open=True, slip_limit=True, slip_match=True, \u0026amp;emspslip_out=False) 配置滑点为基于百分比 set_slippage_fixed( fixed, slip_open=True, slip_limit=True, slip_match=True, slip_out=False) 配置滑点为固定点数 get_orders_open(safe=False) 返回仍然打开的订单（未执行或部分执行）的可迭代对象。返回的订单不得被触摸。如果需要订单操作，请将参数 safe 设置为 True getcommissioninfo(data) 检索与给定数据相关的 CommissionInfo 方案 setcommission( commission=0.0, margin=None, mult=1.0, commtype=None, percabs=True, stocklike=False, interest=0.0, interest_long=False, leverage=1.0, automargin=False, name=None) 为 Broker 设置 CommissionInfo 对象，参考 CommInfoBase 文档介绍。如果 name 为 None，这将是没有找到其他 CommissionInfo 方案的资产的默认设置 addcommissioninfo( comminfo, name=None) 添加 CommissionInfo 对象，如果 name 为 None，将成为所有资产的默认设置 getposition(data) 返回给定数据的当前头寸状态（一个 Position 实例） get_fundshares() 返回基金模式中的当前股票数量 get_fundvalue() 返回基金式的股票价值 add_cash(cash) 添加/移除系统现金（使用负值移除） ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/10-broker/01-broker/","section":"教程","summary":"类 backtrader.brokers.BackBroker() # Broker 经纪商模拟器，支持不同的订单类型，检查订单现金需求，跟踪每次 Cerebro 迭代的现金和价值，并维护各数据的当前头寸。\n","title":"Broker 经纪商详解","type":"docs"},{"content":"策略提供交易方法：buy、sell 和 close。先看 buy 的签名：\ndef buy(self, data=None, size=None, price=None, plimit=None, exectype=None, valid=None, tradeid=0, **kwargs): 注意，如果调用者没有指定 size，则 size 的默认值为 None。这就是 Sizer 发挥作用的地方：\nsize=None 表示策略向其 Sizer 询问实际的头寸大小 这意味着策略有一个 Sizer：是的，确实如此！如果用户没有添加 Sizer，后台会为策略添加一个默认的 Sizer。默认的 Sizer 是 SizerFix。其定义如下：\nclass SizerFix(SizerBase): params = ((\u0026#39;stake\u0026#39;, 1),) 可以猜到，这个 Sizer 只是使用 1 个单位（无论是股票、合约等）进行买卖。\n使用 Sizers # 通过 Cerebro # Sizers 可以通过 Cerebro 的两种方法添加：\naddsizer(sizercls, *args, **kwargs)：添加一个 Sizer，适用于所有添加到 cerebro 的策略。这就是所谓的默认 Sizer。例如： cerebro = bt.Cerebro() cerebro.addsizer(bt.sizers.SizerFix, stake=20) # 默认策略的 Sizer addsizer_byidx(idx, sizercls, *args, **kwargs)：将 Sizer 添加到 idx 引用的策略中。 这个 idx 可以通过 addstrategy 的返回值获取。例如：\ncerebro = bt.Cerebro() cerebro.addsizer(bt.sizers.SizerFix, stake=20) # 默认策略的 Sizer idx = cerebro.addstrategy(MyStrategy, myparam=myvalue) cerebro.addsizer_byidx(idx, bt.sizers.SizerFix, stake=5) cerebro.addstrategy(MyOtherStrategy) 在这个例子中：\n系统添加了一个默认 Sizer，适用于所有没有分配特定 Sizer 的策略。 MyStrategy 在获取其 idx 后，添加了一个特定的 Sizer（更改了 stake 参数）。 第二个策略 MyOtherStrategy 没有特定的 Sizer。 这意味着：\nMyStrategy 最终拥有一个内部特定的 Sizer。 MyOtherStrategy 使用默认 Sizer。 注意：默认并不意味着策略共享同一个 Sizer 实例。每个策略都获得不同的默认 Sizer 实例。要共享单个实例，Sizer 需要是一个单例类。如何定义单例超出了 backtrader 的范围。\n从策略 # Strategy 类提供了 setsizer 和 getsizer 方法（以及 sizer 属性）来管理 Sizer。签名：\ndef setsizer(self, sizer): 接受一个已实例化的 Sizer def getsizer(self): 返回当前的 Sizer 实例，sizer 是可以直接获取/设置的属性\n在这种情况下，Sizer 可以：\n作为参数传递给策略 在 __init__ 中使用 sizer 属性或 setsizer 设置，例如： class MyStrategy(bt.Strategy): params = ((\u0026#39;sizer\u0026#39;, None),) def __init__(self): if self.p.sizer is not None: self.sizer = self.p.sizer 这样可以在与 cerebro 调用相同的级别创建 Sizer，并将其作为参数传递给所有策略，从而实现 Sizer 共享。\nSizer 开发 # 开发一个 Sizer 很简单：\n从 backtrader.Sizer 子类化。 这使你可以访问 self.strategy 和 self.broker，尽管大多数情况下不需要。可以通过经纪商访问的内容：\n数据的头寸，通过 self.strategy.getposition(data) 完整的投资组合价值，通过 self.broker.getvalue() 也可以通过 self.strategy.broker.getvalue() 实现同样的效果。\n重写 _getsizing(self, comminfo, cash, data, isbuy) 方法。 参数：\ncomminfo：包含数据佣金信息的 CommissionInfo 实例，可用于计算头寸价值、操作成本和操作佣金。 cash：经纪商的当前可用现金。 data：操作的目标。 isbuy：买入操作为 True，卖出操作为 False。 此方法返回买入/卖出操作的期望数量。\n返回值的符号无关紧要。例如：如果是卖出操作（isbuy 为 False），方法可以返回 5 或 -5。卖出操作只使用绝对值。\nSizer 已经向经纪商请求了给定数据的佣金信息和实际现金水平，并提供了操作目标数据的直接引用。\n让我们定义 FixedSize Sizer：\nimport backtrader as bt class FixedSize(bt.Sizer): params = ((\u0026#39;stake\u0026#39;, 1),) def _getsizing(self, comminfo, cash, data, isbuy): return self.params.stake 这非常简单，因为 Sizer 不进行计算，只是直接返回参数。\n但该机制应该能够构建复杂的头寸系统来管理进出市场时的头寸。\n另一个例子：一个头寸反转器：\nclass FixedReverser(bt.FixedSize): def _getsizing(self, comminfo, cash, data, isbuy): position = self.broker.getposition(data) size = self.p.stake * (1 + (position.size != 0)) return size 这个 Sizer 继承了 FixedSize，覆盖了 _getsizing：\n通过 broker 属性获取数据的头寸。 根据 position.size 决定是否加倍固定头寸。 返回计算的值。 这样策略就不需要决定是否反转头寸或开仓，Sizer 负责控制，可以随时替换而不影响策略逻辑。\n实用 Sizer 应用 # 不考虑复杂的头寸算法，可以使用两种不同的 Sizer 将策略从仅做多转换为多空策略。只需在 cerebro 执行中更改 Sizer，策略的行为就会发生变化。一个非常简单的收盘价交叉 SMA 策略：\nclass CloseSMA(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 15),) def __init__(self): sma = bt.indicators.SMA(self.data, period=self.p.period) self.crossover = bt.indicators.CrossOver(self.data, sma) def next(self): if self.crossover \u0026gt; 0: self.buy() elif self.crossover \u0026lt; 0: self.sell() 注意策略并没有考虑当前头寸（通过 self.position 检查）来决定是否执行买入或卖出操作。只有交叉信号被考虑，Sizer 将负责所有事务。\n这个 Sizer 仅在卖出时（如果已有头寸）返回非零头寸大小：\nclass LongOnly(bt.Sizer): params = ((\u0026#39;stake\u0026#39;, 1),) def _getsizing(self, comminfo, cash, data, isbuy): if isbuy: return self.p.stake # 卖出情况 position = self.broker.getposition(data) if not position.size: return 0 # 如果没有持仓则不卖出 return self.p.stake 将所有内容放在一起（假设已导入 backtrader 并向系统添加了数据）：\ncerebro.addstrategy(CloseSMA) cerebro.addsizer(LongOnly) cerebro.run() 图表（来自源代码中的示例以测试这一点）。\n多空版本只需将 Sizer 更改为上面显示的 FixedReverser：\ncerebro.addstrategy(CloseSMA) cerebro.addsizer(FixedReverser) cerebro.run() 输出图表。\n注意差异：\n交易次数翻倍。 现金水平从未恢复到初始值，因为策略始终在市场内。 两种方法都有负面效果，但这只是一个示例。\nbt.Sizer 参考 # class backtrader.Sizer() 这是 Sizer 的基类。任何 Sizer 都应继承此类并覆盖 _getsizing 方法。\n成员属性：\nstrategy：由 Sizer 所在策略设置。可以访问策略的整个 API，例如获取实际数据头寸： position = self.strategy.getposition(data) broker：由 Sizer 所在策略设置。复杂 Sizer 可能需要的信息（如投资组合价值等）可以通过它访问。 def _getsizing(comminfo, cash, data, isbuy) 此方法必须由 Sizer 的子类覆盖以提供数量计算功能。\n参数：\ncomminfo：包含数据佣金信息的 CommissionInfo 实例，用于计算头寸价值、操作成本和操作佣金。 cash：经纪商的当前可用现金。 data：操作的目标。 isbuy：买入操作为 True，卖出操作为 False。 此方法必须返回要执行的实际数量（整数）。如果返回 0，则不执行任何操作。\n返回值的绝对值将被使用。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/14-sizers/01-sizers/","section":"教程","summary":"策略提供交易方法：buy、sell 和 close。先看 buy 的签名：\ndef buy(self, data=None, size=None, price=None, plimit=None, exectype=None, valid=None, tradeid=0, **kwargs): 注意，如果调用者没有指定 size，则 size 的默认值为 None。这就是 Sizer 发挥作用的地方：\n","title":"Sizer 仓位管理详解","type":"docs"},{"content":"backtrader 中，Cerebro 是系统的核心，Strategy 是用户的核心。\nStrategy 的生命周期方法 # 策略可在创建时抛出 backtrader.errors 模块的 StrategySkipError 异常来中断，从而避免在回测中处理该策略。详情请参见”异常”部分。\n构建：__init__ # 在实例化时调用：在此创建指标和其他所需属性。\n示例代码：\ndef __init__(self): self.sma = btind.SimpleMovingAverage(period=15) 启动：start # Cerebro 通知策略开始运行。默认有一个空方法。\n初期：prenext # 在 __init__ 中声明的指标会决定策略何时进入成熟期，该周期称为最小周期。上述 __init__ 创建了一个周期为 15 的简单移动平均线 (SMA)。\n只要系统看到的 bar 少于 15 个，就会调用 prenext（默认实现为空操作）。\n成熟：next # 系统积累 15 个 bar 且 SMA 有足够数据开始生成值后，策略进入成熟期并可执行。\n还有一个 nextstart 方法，会在从 prenext 切换到 next 时调用一次。nextstart 的默认实现是直接调用 next。\n繁衍：无 # 策略本身不会繁衍，但优化时系统会用不同参数多次实例化它们。\n结束：stop # 系统通知策略重置并清理。默认有一个空方法。\n常规用法如下：\nclass MyStrategy(bt.Strategy): def __init__(self): self.sma = btind.SimpleMovingAverage(period=15) def next(self): if self.sma \u0026gt; self.data.close: # 执行某些操作 pass elif self.sma \u0026lt; self.data.close: # 执行其他操作 pass 在这个代码片段中：\n在 __init__ 中分配一个指标给属性 默认的空 start 方法未被重写 prenext 和 nextstart 未被重写 在 next 中，指标的值与收盘价进行比较，以执行某些操作 默认的空 stop 方法未被重写 策略事件通知 # 策略在每个 next 周期收到通知：\nnotify_order # 通过 notify_order(order) 接收任何订单状态的变化通知。\nnotify_trade # 通过 notify_trade(trade) 接收任何交易的开启/更新/关闭通知\nnotify_cashvalue # 通过 notify_cashvalue(cash, value) 接收经纪账户中的当前现金和投资组合\nnotify_fund # 通过 notify_fund(cash, value, fundvalue, shares) 接收经纪账户中的当前现金、投资组合、基金价值和份额\nnotify_store # 通过 notify_store(msg, *args, **kwargs) 接收来自存储提供者的通知\n如何买入/卖出/平仓 # buy 和 sell 方法生成订单，返回一个 Order（或其子类）实例作为引用。该订单有唯一的 ref 标识符用于比较。\n特定经纪实现的 Order 子类可能携带经纪提供的其他唯一标识符。\n要创建订单，请使用以下参数：\n参数名 默认值 描述 data None 创建订单的数据。如果为 None，则使用系统中的第一个数据 self.datas[0] 或 self.data0（即 self.data）。 size None 要使用的订单数量（正值）。如果为 None，则使用通过 getsizer 检索到的 sizer 实例来确定大小。 price None） 使用的价格（实时经纪可能对格式有最低价格步长要求）。对于市场和收盘订单（Market 和 Close orders）为 None（市场决定价格）。对于限价、止损和止损限价订单，这个值决定触发点。 plimit None 仅适用于止损限价订单。这是在触发止损后设置隐含限价订单的价格（使用 price）。 exectype None 可能的值：- Order.Market 或 None：市场订单将以下一个可用价格执行。在回测中，将是下一个 bar 的开盘价。- Order.Limit：订单只能以给定价格或更好价格执行。- Order.Stop：订单在 price 触发时被触发，并像 Order.Market 订单一样执行。- Order.StopLimit：订单在 price 触发时被触发，并作为隐含的限价订单执行，限价由 pricelimit 给出。 valid None 可能的值：- None：生成一个不会过期的订单（即 Good til cancel），并保留在市场上直到匹配或取消。实际上，经纪通常会施加一个时间限制，但通常远远在未来，可以认为它不会过期。- datetime.datetime 或 datetime.date 实例：日期将用于生成有效期至给定日期的订单（即 good til date）。- Order.DAY 或 0 或 timedelta()：生成一个有效期至交易日结束的订单（即日订单）。- 数值：假定为 matplotlib 编码的 datetime 值（backtrader 使用的），将用于生成有效期至该时间的订单（即 good til date）。 tradeid 0 backtrader 用于跟踪同一资产上重叠交易的内部值。当通知订单状态变化时，此 tradeid 会发送回策略。 例如，如果 backtrader 直接支持的 4 种订单执行类型不够，可以为 Interactive Brokers 传递以下参数：\norderType=\u0026#39;LIT\u0026#39;, lmtPrice=10.0, auxPrice=9.8 这将覆盖 backtrader 创建的设置，并生成一个 LIMIT IF TOUCHED 订单，触及价格为 9.8，限价为 10.0。\n策略信息： # 属性名 描述 env 策略所属的 cerebro 实体 datas 传递给 cerebro 的数据源数组 data/data0 datas[0] 的别名 dataX datas[X] 的别名 dnames 按名称访问数据源的替代方法（通过 [name] 或 .name 语法） broker 与此策略关联的经纪（从 cerebro 接收） stats 包含 cerebro 为此策略创建的观察者的列表/命名元组序列 analyzers 包含 cerebro 为此策略创建的分析器的列表/命名元组序列 position 获取 data0 的当前仓位的属性 成员属性（用于统计/观察者/分析器）： # 属性名 描述 _orderspending 将在调用 next 之前通知策略的订单列表 _tradespending 将在调用 next 之前通知策略的交易列表 _orders 已经通知的订单列表。订单可以在列表中多次出现，具有不同的状态和执行部分。列表旨在保留历史记录。 _trades 已经通知的交易列表。交易可以像订单一样多次出现在列表中。 prenext、nextstart 和 next 可以针对同一时间点多次调用（例如，当使用每日时间框架时，价格更新每日 bar 的 ticks）。\n策略参考 # class backtrader.Strategy(*args, **kwargs)\n基类，用于子类化用户定义的策略。\n方法 # 方法名 描述 next() 当所有数据/指标的最小周期满足时，将为所有剩余数据点调用此方法。 nextstart() 当所有数据/指标的最小周期满足时，将调用一次此方法。默认行为是调用 next。 prenext() 在满足所有数据/指标的最小周期之前调用此方法。 start() 在回测即将开始之前调用。 stop() 在回测即将结束之前调用。 notify_order(order) 当订单状态变化时接收通知。 notify_trade(trade) 当交易状态变化时接收通知。 notify_cashvalue(cash, value) 接收策略经纪账户的当前资金和投资组合状态。 notify_fund(cash, value, fundvalue, shares) 接收当前现金、投资组合、基金价值和份额。 notify_store(msg, *args, **kwargs) 接收存储提供者的通知。 订单方法 # 方法名 描述 buy(...) 创建买入订单并发送给经纪。 sell(...) 创建卖出（空头）订单并发送给经纪。 close(...)： 关闭现有仓位。 cancel(order) 取消经纪中的订单。 其他方法 # 方法名 描述 buy_bracket(...) 创建括号订单组（低侧 - 买入订单 - 高侧）。 sell_bracket(...) 创建括号订单组（低侧 - 卖出订单 - 高侧）。 order_target_size(...) 下订单将仓位重新平衡为目标大小。 order_target_value(...) 下订单将仓位重新平衡为目标价值。 order_target_percent(...) 下订单将仓位重新平衡为当前投资组合价值的目标百分比。 getsizer() 返回用于自动计算头寸的 sizer。 setsizer(sizer) 替换默认的 sizer。 getsizing(data=None, isbuy=True) 返回 sizer 实例为当前情况计算的头寸。 getpositionbyname(name=None, broker=None) 按名称返回给定经纪的当前仓位。 getposition(data=None, broker=None) 返回给定数据和经纪的当前仓位。 getpositionsbyname(broker=None) 直接从经纪获取按名称的所有仓位。 getdatanames() 返回现有数据名称的列表。 getdatabyname(name) 使用环境（cerebro）按名称返回给定数据。 add_timer(...) 计划一个计时器以调用指定的回调或策略的 notify_timer。 notify_timer(timer, when, *args, **kwargs) 接收计时器通知，其中 timer 是由 add_timer 返回的计时器，when 是调用时间。args 和 kwargs 是传递给 add_timer 的额外参数。 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/07-strategy/01-strategy/","section":"教程","summary":"backtrader 中，Cerebro 是系统的核心，Strategy 是用户的核心。\nStrategy 的生命周期方法 # 策略可在创建时抛出 backtrader.errors 模块的 StrategySkipError 异常来中断，从而避免在回测中处理该策略。详情请参见”异常”部分。\n","title":"Strategy 策略基类详解","type":"docs"},{"content":" Cerebro 是 backtrader 的核心控制系统，而 Strategy（由用户子类化）是用户的主要操作入口。策略需要连接系统的其他部分，订单正是为此而生。\n订单将策略的逻辑决策转化为 Broker 可执行的消息，通过以下方式完成：\n创建 # 通过 Strategy 的方法：buy、sell 和 close（Strategy），这些方法返回一个订单实例作为参考。\n取消 # 通过 Strategy 的方法：cancel（Strategy），该方法需要一个订单实例来操作。\n订单也用于向用户反馈 Broker 中的执行情况。\n通知 # 通过 Strategy 的方法：notify_order（Strategy），该方法报告一个订单实例。\n订单创建 # 调用 buy、sell 和 close 时，可用以下参数：\n参数名 默认值 描述 data None 为哪个数据创建订单。如果为 None，则使用系统中的第一个数据，self.datas[0] 或 self.data0（又名 self.data）。 size None 使用的单位数量。如果为 None，则使用通过 getsizer 获取的 sizer 实例来确定大小。 price None 使用的价格（实时 Broker 可能会对格式有实际限制，如果不符合最小刻度要求）。对于 Market 和 Close 订单，None 是有效的（市场决定价格）。对于 Limit、Stop 和 StopLimit 订单，该值决定触发点（在 Limit 的情况下，触发点显然是订单匹配的价格）。 plimit None 仅适用于 StopLimit 订单。这是在 Stop 触发后设置隐含 Limit 订单的价格。 exectype None 可能的值：- Order.Market 或 None：市场订单将以下一个可用价格执行。在回测中，这将是下一根K线的开盘价。- Order.Limit：只能在给定价格或更好的价格执行的订单。- Order.Stop：在价格触发时执行的订单，执行方式如同 Market 订单。- Order.StopLimit：在价格触发时执行的订单，作为隐含 Limit 订单执行，价格由 pricelimit 给定。 valid None 可能的值：- None：生成一个不会过期的订单（即 Good till cancel），并在市场中保留直到匹配或取消。实际上，Broker 往往会强制一个时间限制，但这通常是很长时间，所以认为它不会过期。- datetime.datetime 或 datetime.date 实例：使用给定的日期生成一个有效直到该日期的订单（即 Good till date）。- Order.DAY 或 0 或 timedelta()：生成一个有效期为一天的订单（即日订单），有效期直到会话结束。- 数值：假定为一个对应于 matplotlib 编码的日期时间值（backtrader 使用的），并用于生成一个有效期至该时间的订单（即 Good till date）。 tradeid 0 这是 backtrader 用来跟踪同一资产上重叠交易的内部值。在通知订单状态变化时，该 tradeid 会返回给策略。 **kwargs / 额外的 Broker 实现可能支持额外的参数。backtrader 会将 kwargs 传递给创建的订单对象。 示例 # 如果 backtrader 默认的 4 种订单执行类型不够用，例如对接 Interactive Brokers 时，可以传递如下参数：\norderType=\u0026#39;LIT\u0026#39;, lmtPrice=10.0, auxPrice=9.8 这将覆盖 backtrader 的设置，生成一个触及价格为 9.8 且限价为 10.0 的 LIMIT IF TOUCHED 订单。\n注意：\nclose 方法会检查当前仓位，自动使用 buy 或 sell 平仓。如果不传参数，大小会自动计算；传入参数则可实现部分平仓或反向开仓。\n订单通知 # 要接收通知，需要在自定义的 Strategy 子类中重写 notify_order 方法（默认什么都不做）。通知规则如下：\n在策略的 next 方法之前发出。 在同一个 next 循环中，同一订单可能（且通常会）多次通知不同状态。 订单可能被提交、接受并在下一次 next 调用前完成执行。 这种情况下至少会有 3 次通知，状态依次为：\nOrder.Submitted：订单已发送给 Broker。 Order.Accepted：Broker 已接受订单，等待执行。 Order.Completed：订单已快速匹配并完全成交（常见于 Market 订单）。 Order.Partial 状态在回测 Broker 中不会出现（不考虑部分成交），但在实际 Broker 中很常见。\n实际 Broker 可能在更新仓位前发出一次或多次执行通知，这些执行构成一个 Order.Partial 通知。\n实际执行数据保存在 order.executed 属性中，这是一个 OrderData 类型对象，包含大小、价格等常见字段。\n创建时的值保存在 order.created 中，在整个订单生命周期内保持不变。\n订单状态值 # 以下定义：\nOrder.Created：创建订单实例时设置。除非手动创建，否则用户不会看到此状态。 Order.Submitted：订单已发送给 Broker。回测模式会立即触发，但实际 Broker 可能需要时间，可能在转发给交易所时才通知。 Order.Accepted：Broker 已接受订单，正在系统中（或交易所）等待执行，包含执行类型、大小、价格和有效期等参数。 Order.Partial：订单已部分执行。order.executed 包含当前成交的大小和平均价格。 order.executed.exbits 包含部分成交的完整执行位列表。 Order.Complete：订单已完全成交。 Order.Rejected：Broker 拒绝了订单，可能因为某个参数（如有效期）不被接受。 原因通过策略的 notify_store 方法通知。实际 Broker 会通过事件通知，可能与订单直接相关也可能不相关。 回测 Broker 中不会出现此状态。 Order.Margin：订单执行会导致保证金不足，之前接受的订单已从系统中移除。 Order.Cancelled（或 Order.Canceled）：用户取消请求的确认。 注意，调用 cancel 方法并不保证订单一定会被取消。订单可能已执行，但 Broker 尚未通知策略。 Order.Expired：之前接受的订单已过期，已被从系统中移除。 引用：Order 及相关类 # 这些是 backtrader 生态系统中的通用类。与不同 Broker 集成时，它们可能被扩展或包含额外信息，请参阅相应 Broker 的文档。\nclass backtrader.order.Order() 包含创建和执行数据以及订单类型的类。\n订单可能具有以下状态：\nSubmitted：发送给 Broker，等待确认。 Accepted：被 Broker 接受。 Partial：部分执行。 Completed：完全执行。 Canceled/Cancelled：被用户取消。 Expired：过期。 Margin：没有足够的现金执行订单。 Rejected：被 Broker 拒绝。 可能发生在订单提交期间（因此不会达到 Accepted 状态），或执行前因价格变动导致现金被其他用途（如期货保证金）占用。 成员属性：\nref：唯一订单标识符。 created：持有创建数据的 OrderData。 executed：持有执行数据的 OrderData。 info：通过 addinfo() 方法传递的自定义信息。以 OrderedDict 子类形式保存，键也可用 . 符号访问。 用户方法：\nisbuy()：返回布尔值，判断是否为买入订单。 issell()：返回布尔值，判断是否为卖出订单。 alive()：如果订单状态为 Partial 或 Accepted，返回 True。 class backtrader.order.OrderData(dt=None, size=0, price=0.0, pricelimit=0.0, remsize =0, pclose=0.0, trailamount=0.0, trailpercent=0.0) 包含订单创建和执行的实际数据。\n创建时保存的是请求参数，执行时保存的是实际结果。\n成员属性：\nexbits：此 OrderData 的 OrderExecutionBits 的可迭代对象。 dt：日期时间（浮点数）创建/执行时间。 size：请求/执行大小。 price：执行价格。注意：如果没有价格和 pricelimit，订单创建时的收盘价将作为参考。 pricelimit：StopLimit 的价格限制（首先有触发）。 trailamount：追踪止损的绝对价格距离。 trailpercent：追踪止损的百分比价格距离。 value：整个位大小的市场价值。 comm：整个位执行的佣金。 pnl：此位产生的盈亏（如果有关闭）。 margin：订单产生的保证金（如果有）。 psize：当前未平仓头寸大小。 pprice：当前未平仓头寸价格。 class backtrader.order.OrderExecutionBit(dt=None, size=0, price=0.0, closed=0, closedvalue=0.0, closedcomm=0.0, opened=0, openedvalue=0.0, openedcomm=0.0, pnl=0.0, psize=0, pprice=0.0) 旨在持有订单执行信息。“位”不确定订单是否已完全/部分执行，只是持有信息。\n成员属性：\ndt：日期时间（浮点数）执行时间。 size：执行数量。 price：执行价格。 closed：执行关闭了多少现有头寸。 opened：执行打开了多少新头寸。 openedvalue：新开部分的市场价值。 closedvalue：关闭部分的市场价值。 closedcomm：关闭部分的佣金。 openedcomm：打开部分的佣金。 value：整个位大小的市场价值。 comm：整个位执行的佣金。 pnl：此位产生的盈亏（如果有关闭）。 psize：当前未平仓头寸大小。 pprice：当前未平仓头寸价格。 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/09-orders/01-general/","section":"教程","summary":" Cerebro 是 backtrader 的核心控制系统，而 Strategy（由用户子类化）是用户的主要操作入口。策略需要连接系统的其他部分，订单正是为此而生。\n","title":"订单系统详解","type":"docs"},{"content":"最近 Reddit 上两个相关帖子启发了本文：\n声称 backtrader 无法处理 160 万根 K 线的帖子：reddit/r/algotrading - A performant backtesting system? 需要一个能回测 8000 支股票的工具：reddit/r/algotrading - Backtesting libs that supports 1000+ stocks? 其中一位作者问如何使用能回测”超大内存”的框架，”因为显然不能将所有数据加载到内存中。”\n本文将通过 backtrader 来讨论这些概念。\n2M K线 # 为了验证，首先生成这么多 K 线。第一个帖子提到 77 支股票和 160 万根 K 线，每支约 20,779 根。因此我们简化如下：\n为 100 支股票生成 K线数据 每支股票生成 20,000 根 K线 即：生成 100 个文件，总共 200 万根 K线。 生成数据的脚本如下：\nimport numpy as np import pandas as pd COLUMNS = [\u0026#39;open\u0026#39;, \u0026#39;high\u0026#39;, \u0026#39;low\u0026#39;, \u0026#39;close\u0026#39;, \u0026#39;volume\u0026#39;, \u0026#39;openinterest\u0026#39;] CANDLES = 20000 STOCKS = 100 dateindex = pd.date_range(start=\u0026#39;2010-01-01\u0026#39;, periods=CANDLES, freq=\u0026#39;15min\u0026#39;) for i in range(STOCKS): data = np.random.randint(10, 20, size=(CANDLES, len(COLUMNS))) df = pd.DataFrame(data * 1.01, dateindex, columns=COLUMNS) df = df.rename_axis(\u0026#39;datetime\u0026#39;) df.to_csv(\u0026#39;candles{:02d}.csv\u0026#39;.format(i)) 脚本生成了 100 个文件（candles00.csv 到 candles99.csv）。数据值并不重要，关键是保持标准的日期时间格式和 OHLCV 数据（含未平仓合约）。\n测试系统 # 硬件/操作系统：使用的是一台 Windows 10 15.6 英寸的笔记本，配备 Intel i7 处理器和 32GB 内存。\nPython 版本：CPython 3.6.1 和 pypy3 6.0.0。\n其他：持续运行的应用程序占用约 20% 的 CPU，浏览器（Chrome 102 个进程）、Edge、Word、PowerPoint、Excel 及其他一些小程序正在运行。\nbacktrader 默认配置 # 让我们回顾一下 backtrader 的默认运行时配置：\n如果可能，预加载所有数据源 如果所有数据源都可以预加载，则以批处理模式运行（命名为 runonce） 先计算所有指标 按步骤执行策略逻辑和经纪人 默认批处理模式执行 # 测试脚本（见完整源码）将打开 100 个文件并用 backtrader 默认配置处理。\n$ ./two-million-candles.py Cerebro Start Time: 2019-10-26 08:33:15.563088 Strat Init Time: 2019-10-26 08:34:31.845349 Time Loading Data Feeds: 76.28 Number of data feeds: 100 Strat Start Time: 2019-10-26 08:34:31.864349 Pre-Next Start Time: 2019-10-26 08:34:32.670352 Time Calculating Indicators: 0.81 Next Start Time: 2019-10-26 08:34:32.671351 Strat warm-up period Time: 0.00 Time to Strat Next Logic: 77.11 End Time: 2019-10-26 08:35:31.493349 Time in Strategy Next Logic: 58.82 Total Time in Strategy: 58.82 Total Time: 135.93 Length of data feeds: 20000 Memory Usage: A peak of 348 Mbytes was observed 大部分时间实际上是在预加载数据（98.63 秒），其余时间用于策略执行（包括在每次迭代中通过经纪人进行处理，耗时 73.63 秒）。总时间为 173.26 秒。\n性能为：每秒 14,713 根 K线。\n结论：上述第一个 Reddit 线程中声称 backtrader 无法处理 160 万根 K线的说法是错误的。\n使用 pypy # 既然有帖子说用 pypy 没帮助，来看看实际表现。\n$ ./two-million-candles.py Cerebro Start Time: 2019-10-26 08:39:42.958689 Strat Init Time: 2019-10-26 08:40:31.260691 Time Loading Data Feeds: 48.30 Number of data feeds: 100 Strat Start Time: 2019-10-26 08:40:31.338692 Pre-Next Start Time: 2019-10-26 08:40:31.612688 Time Calculating Indicators: 0.27 Next Start Time: 2019-10-26 08:40:31.612688 Strat warm-up period Time: 0.00 Time to Strat Next Logic: 48.65 End Time: 2019-10-26 08:40:40.150689 Time in Strategy Next Logic: 8.54 Total Time in Strategy: 8.54 Total Time: 57.19 Length of data feeds: 20000 总时间已经从 135.93 秒降到了 57.19 秒，性能提升了超过一倍。\n性能为：每秒 34,971 根 K线。\n内存使用：最大内存峰值为 269 MB。\n这也相较于标准 CPython 解释器，内存使用有所改善。\n处理 200 万根 K 线时的超大内存 # 可以通过 backtrader 的配置选项进一步优化，包括优化缓冲区、仅使用最小数据集（理想情况下缓冲区大小为 1）。\n优化选项为 exactbars=True。文档描述如下：\nTrue 或 1：所有”线”对象将内存使用减少到自动计算的最小周期。 如果简单移动平均线周期为 30，底层数据将始终保留 30 根 K 线的运行缓冲区以计算该均线。 此设置会禁用 preload 和 runonce。\n由于绘图会被禁用，还可使用 stdstats=False 禁用标准观察器（现金、资产价值和交易）。\n$ ./two-million-candles.py --cerebro exactbars=False,stdstats=False Cerebro Start Time: 2019-10-26 08:37:08.014348 Strat Init Time: 2019-10-26 08:38:21.850392 Time Loading Data Feeds: 73.84 Number of data feeds: 100 Strat Start Time: 2019-10-26 08:38:21.851394 Pre-Next Start Time: 2019-10-26 08:38:21.857393 Time Calculating Indicators: 0.01 Next Start Time: 2019-10-26 08:38:21.857393 Strat warm-up period Time: 0.00 Time to Strat Next Logic: 73.84 End Time: 2019-10-26 08:39:02.334936 Time in Strategy Next Logic: 40.48 Total Time in Strategy: 40.48 Total Time: 114.32 Length of data feeds: 20000 性能为：每秒 17,494 根 K线。\n内存使用：固定为 75 MB。\n与之前未经优化的运行对比，预加载数据的时间已经不再是瓶颈，回测过程几乎可以立刻开始。\n总时间为 114.32 秒，相较于 135.93 秒，改善了 15.90%。\n内存使用改善了 68.5%。\n再次使用 pypy # 现在我们已经知道如何优化，接下来再次用 pypy 来做测试。\n$ ./two-million-candles.py --cerebro exactbars=True,stdstats=False Cerebro Start Time: 2019-10-26 08:44:32.309689 Strat Init Time: 2019-10 -26 08:45:22.177687 Time Loading Data Feeds: 58.80 Number of data feeds: 100 Strat Start Time: 2019-10-26 08:45:22.178689 Pre-Next Start Time: 2019-10-26 08:45:22.181688 Time Calculating Indicators: 0.26 Next Start Time: 2019-10-26 08:45:22.181688 Strat warm-up period Time: 0.00 Time to Strat Next Logic: 58.79 End Time: 2019-10-26 08:45:42.316689 Time in Strategy Next Logic: 20.14 Total Time in Strategy: 20.14 Total Time: 78.90 Length of data feeds: 20000 性能：每秒 50,956 根 K线。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/19-articles/01-out-of-memory/","section":"教程","summary":"最近 Reddit 上两个相关帖子启发了本文：\n声称 backtrader 无法处理 160 万根 K 线的帖子：reddit/r/algotrading - A performant backtesting system? 需要一个能回测 8000 支股票的工具：reddit/r/algotrading - Backtesting libs that supports 1000+ stocks? 其中一位作者问如何使用能回测”超大内存”的框架，”因为显然不能将所有数据加载到内存中。”\n","title":"回测中的超大内存问题与优化","type":"docs"},{"content":"回测虽然是基于计算的自动化过程，但可视化仍然非常重要。无论是已有算法的回测结果，还是数据产生的各种指标，绘图都能帮助你理解发生了什么，进而优化或产生新想法。\n因此，backtrader 利用 matplotlib 内置了绘图功能，可以绘制数据馈送、指标、交易操作、现金流和投资组合价值的变化。\n如何绘图 # 任何回测运行都可以通过调用一个方法进行绘图：\ncerebro.plot() 当然，这通常是最后一个命令。例如，以下简单代码使用了 backtrader 源代码中的一个示例数据：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt class St(bt.Strategy): def __init__(self): self.sma = bt.indicators.SimpleMovingAverage(self.data) data = bt.feeds.BacktraderCSVData(dataname=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;) cerebro = bt.Cerebro() cerebro.adddata(data) cerebro.addstrategy(St) cerebro.run() cerebro.plot() 这将生成以下图表：\n图表包含了 3 个观察器，由于缺乏任何交易，它们在这种情况下几乎没有意义：\nCashValue 观察器：跟踪回测期间的现金和投资组合总价值（含现金）。 Trade 观察器：交易结束时显示实际盈亏。交易定义为开仓后仓位归零（包括多空转换）。 BuySell 观察器：在价格图上标记买入和卖出操作的位置。 这 3 个观察器由 cerebro 自动添加，可通过 stdstats 参数控制（默认：True）。如需禁用：\ncerebro = bt.Cerebro(stdstats=False) 或在运行时：\ncerebro = bt.Cerebro() ... cerebro.run(stdstats=False) 绘图元素 # 除观察器外，以下 3 种元素也会被绘制：\n通过 adddata、replaydata 和 resampledata 添加到 Cerebro 的数据馈送。 在策略中声明的指标（或通过 addindicator 添加到 cerebro 的指标，用于实验目的）。 通过 addobserver 添加到 cerebro 的观察器。 观察器是与策略同步运行的线性对象，可访问整个生态系统，用于跟踪现金和价值等指标。\n绘图选项 # 指标和观察器有几个选项可以控制它们在图表上的绘制方式，分为 3 大类：\n影响整个对象绘图行为的选项。 影响单个线条绘图行为的选项。 影响系统级别绘图选项的选项。 对象级绘图选项 # 这些选项由指标和观察器中的 plotinfo 数据集控制：\nplotinfo = dict( plot=True, subplot=True, plotname=\u0026#39;\u0026#39;, plotskip=False, plotabove=False, plotlinelabels=False, plotlinevalues=True, plotvaluetags=True, plotymargin=0.0, plotyhlines=[], plotyticks=[], plothlines=[], plotforce=False, plotmaster=None, plotylimited=True, ) 虽然 plotinfo 在类定义中显示为字典，但 backtrader 的元类机制会将其转换为可继承的对象，支持多重继承。例如：\n如果子类将 subplot=True 改为 subplot=False，则后续子类会以 False 作为 subplot 的默认值。\n有两种方法为这些参数赋值。让我们看一下 SimpleMovingAverage 实例化的第一种方法：\nsma = bt.indicators.SimpleMovingAverage(self.data, period=15, plotname=\u0026#39;mysma\u0026#39;) 从示例可知，SimpleMovingAverage 构造函数未使用的 **kwargs 会尝试解析为 plotinfo 值。SimpleMovingAverage 只定义了一个参数 period，因此 plotname 会匹配 plotinfo 中同名参数。\n第二种方法：\nsma = bt.indicators.SimpleMovingAverage(self.data, period=15) sma.plotinfo.plotname = \u0026#39;mysma\u0026#39; 与 SimpleMovingAverage 一起实例化的 plotinfo 对象可以被访问，并且其中的参数也可以通过标准的 Python 点符号访问。这种语法更易于理解。\n参数含义 # plot：是否绘制该对象。 subplot：是否与数据一起绘制或在独立的子图中绘制。例如，移动平均线是绘制在数据上的，而随机指标和 RSI 则是在不同的刻度上绘制的。 plotname：在图表上使用的名称，而不是类名。例如上面的 mysma 而不是 SimpleMovingAverage。 plotskip（已弃用）：plot 的旧别名。 plotabove：是否在数据之上绘制，否则在数据下方绘制。这仅在 subplot=True 时有效。 plotlinelabels：是否在图表的图例中显示单个线条的名称，当 subplot=False 时。 plotlinevalues：控制图例中是否包含指标和观察器线条的最后绘制值。可以通过每条线的 _plotvalue 控制。 plotvaluetags：控制是否在线条的右侧绘制带有最后值的标签。可以通过每条线的 _plotvaluetag 控制。 plotymargin：在图表上单个子图顶部和底部添加的边距。它是一个以 1 为基数的百分比。例如：0.05 表示 5%。 plothlines：一个包含值的可迭代对象（在刻度内），在这些值处绘制水平线。例如，经典指标的超买、超卖区域通常在 70 和 30 处绘制线。 plotyticks：一个包含值的可迭代对象（在刻度内），在这些值处特意放置刻度值。例如，强制刻度有 50 以识别刻度的中点。 plotyhlines：一个包含值的可迭代对象（在刻度内），在这些值处绘制水平线。如果未定义上述任何项，那么水平线和刻度的位置将完全由此值控制。 plotforce：如果所有其他方法都失败，这是一个最后的强制绘图机制。 plotmaster：一个指标/观察器有一个主控，它是工作的数据。在某些情况下，可能希望与不同的主控一起绘图。 plotylimited：目前仅适用于数据馈送。如果为 True（默认），其他线条在数据图上的绘图不会改变刻度。 线条特定绘图选项 # 指标/观察器有线条，如何绘制这些线条可以通过 plotlines 对象进行影响。大多数 plotlines 中的选项旨在直接传递给 matplotlib。文档主要通过示例进行说明。\n重要：选项是按每条线条指定的。\n一些选项由 backtrader 直接控制，这些选项都以 _ 开头：\n_plotskip（布尔值）：如果设置为 True，表示跳过绘制特定线条。 _plotvalue（布尔值）：控制此线条的图例是否包含最后绘制的值（默认值为 True）。 _plotvaluetag（布尔值）：控制是否在线条右侧绘制带有最后值的标签（默认值为 True）。 _name（字符串）：更改特定线条的绘图名称。 _skipnan（布尔值，默认：False）：跳过绘制 NaN 值。 _samecolor（布尔值）：强制下一条线条使用与前一条相同的颜色，避免 matplotlib 为每个新绘制的元素循环颜色图。 _method（字符串）：选择 matplotlib 用于绘制元素的方法。 示例：\nMACDHisto 使用 _method='bar' 绘制直方图。\nBuySell 观察器：\nplotlines = dict( buy=dict(marker=\u0026#39;^\u0026#39;, markersize=8.0, color=\u0026#39;lime\u0026#39;, fillstyle=\u0026#39;full\u0026#39;), sell=dict(marker=\u0026#39;v\u0026#39;, markersize=8.0, color=\u0026#39;red\u0026#39;, fillstyle=\u0026#39;full\u0026#39;) ) Trades 观察器： lines = (\u0026#39;pnlplus\u0026#39;, \u0026#39;pnlminus\u0026#39;) plotlines = dict( pnlplus=dict(_name=\u0026#39;Positive\u0026#39;, marker=\u0026#39;o\u0026#39;, color=\u0026#39;blue\u0026#39;, markersize=8.0, fillstyle=\u0026#39;full\u0026#39;), pnlminus=dict(_name=\u0026#39;Negative\u0026#39;, marker=\u0026#39;o\u0026#39;, color=\u0026#39;red\u0026#39;, markersize=8.0, fillstyle=\u0026#39;full\u0026#39;) ) 系统级绘图选项 # cerebro 的 plot 方法签名：\ndef plot(self, plotter=None, numfigs=1, iplot=True, **kwargs): plotter：一个包含控制系统级绘图选项的对象/类。如果为 None，则创建默认 PlotScheme 对象。 numfigs：将图表分解为多少个独立的图表。 iplot：如果在 Jupyter Notebook 中运行，自动内联绘图。 kwargs：将用于更改 plotter 或默认 PlotScheme 对象的属性值。 PlotScheme # 包含控制系统级绘图的所有选项的对象。选项在代码中有文档说明。\n颜色 # PlotScheme 类定义了一个方法，可以在子类中重写，该方法返回要使用的下一个颜色：\ndef color(self, idx) 其中 idx 是当前线条的索引。默认颜色方案是 Tableau 10 颜色调色板，索引修改为：\ntab10_index = [3, 0, 2, 1, 2, 4, 5, 6, 7, 8, 9] ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/16-plotting/01-plotting/","section":"教程","summary":"回测虽然是基于计算的自动化过程，但可视化仍然非常重要。无论是已有算法的回测结果，还是数据产生的各种指标，绘图都能帮助你理解发生了什么，进而优化或产生新想法。\n","title":"绘图系统详解","type":"docs"},{"content":"指标可以在平台中的两个地方使用：\n策略内部 其他指标内部 指标在操作中的使用 # 在策略中，指标总是在 __init__ 中实例化，在 next 中使用或检查指标值。一个重要原则是：在 __init__ 中声明的任何指标（或派生值）都会在调用 next 前预先计算。\n了解两种模式的差异。\n__init__ vs next # 在 __init__ 中，对线条对象的操作都会生成新的线条对象。在 next 中，对线条对象的操作则生成常规的 Python 类型，如浮点数和布尔值。\n例如 __init__ 中的操作：\nhilo_diff = self.data.high - self.data.low 变量 hilo_diff 持有线条对象的引用，该对象在调用 next 前预先计算，可通过标准数组索引 [] 访问。\n它包含数据源中每个条的高低差值。\n这在混合简单线条（如 self.data 数据源中的线条）和复杂线条（如指标）时也有效：\nsma = bt.SimpleMovingAverage(self.data.close) close_sma_diff = self.data.close - sma 现在 close_sma_diff 同样包含一个线条对象。\n使用逻辑运算符：\nclose_over_sma = self.data.close \u0026gt; sma 现在生成的线条对象将包含一个布尔数组。\n在 next 中，一个操作（逻辑运算符）：\nclose_over_sma = self.data.close \u0026gt; self.sma 使用等效数组（基于索引 0 的表示法）：\nclose_over_sma = self.data.close[0] \u0026gt; self.sma[0] 这种情况下，close_over_sma 生成一个布尔值，即比较 self.data.close[0] 和 self.sma[0] 两个浮点数的结果。\n为什么选择 __init__ vs next # 逻辑简化和易用性是关键。可以在 __init__ 中声明计算和大部分相关逻辑，让 next 中的实际操作逻辑保持最小。\n还有一个附带好处：速度（得益于预先计算）。\n一个完整的示例，在 __init__ 中生成一个买入信号：\nclass MyStrategy(bt.Strategy): def __init__(self): sma1 = btind.SimpleMovingAverage(self.data) ema1 = btind.ExponentialMovingAverage() close_over_sma = self.data.close \u0026gt; sma1 close_over_ema = self.data.close \u0026gt; ema1 sma_ema_diff = sma1 - ema1 buy_sig = bt.And(close_over_sma, close_over_ema, sma_ema_diff \u0026gt; 0) def next(self): if buy_sig: self.buy() Python 的 and 运算符无法被重载，因此平台定义了自己的 And。其他构造如 Or 和 If 也是如此。\n“声明式”方法在 __init__ 期间让 next（策略实际工作的地方）保持精简。此外，这也加快了执行速度。\n当逻辑变得复杂并涉及多个操作时，最好将其封装在指标内。\n一些注意事项 # 在上述示例中，与其他平台相比，backtrader 已简化了两件事：\n声明的指标既不需要父参数（如创建它们的策略），也无需调用注册方法或函数。但策略仍会自动触发指标及操作生成的线条对象的计算（如 sma - ema）。\nExponentialMovingAverage 实例化时没有传入 self.data，这是有意为之。如果不传数据，父对象（即策略）的第一个数据会自动传入。\n指标绘图 # 首先要强调的是：声明的指标会自动绘制（如果调用了 cerebro.plot）。操作生成的线条对象不会绘制（如 close_over_sma = self.data.close \u0026gt; self.sma）。\n可以用辅助的 LinePlotterIndicator 来绘制这些操作，用法如下：\nclose_over_sma = self.data.close \u0026gt; self.sma LinePlotterIndicator(close_over_sma, name=\u0026#39;Close_over_SMA\u0026#39;) name 参数为指标的单条线命名。\n控制绘图 # 在开发指标期间，可以添加一个 plotinfo 声明。它可以是一个包含两元素的元组的元组、一个字典或一个有序字典。看起来像这样：\nclass MyIndicator(bt.Indicator): ... plotinfo = dict(subplot=False) ... 稍后可以访问或设置该值（如需）：\nmyind = MyIndicator(self.data, someparam=value) myind.plotinfo.subplot = True 也可以在实例化时设置该值：\nmyind = MyIndicator(self.data, someparams=value, subplot=True) subplot=True 将传递给指标的（在幕后实例化的）成员变量 plotinfo。\nplotinfo 提供以下参数来控制绘图行为：\n参数名 默认值 描述 plot True 是否绘制指标。 subplot True 是否在不同窗口中绘制指标。对于移动平均线等指标，默认值更改为 False。 plotname '' 设置绘图中显示的名称。空值表示将使用指标的规范名称（class.name）。这有一些限制，因为 Python 标识符不能使用算术运算符等。例如，指标 DI+ 将声明如下： plotinfo = dict(plotname='DI+')。 plotabove False 指标通常绘制在其操作的数据下方。将其设置为 True 会使指标绘制在数据上方。 plotlinelabels False 适用于“指标”上的“指标”。如果计算 RSI 的简单移动平均线，绘图通常会显示“SimpleMovingAverage”的名称。这是“指标”的名称，而不是实际绘制的线条的名称。如果将值设置为 True，将使用简单移动平均线内线条的实际名称。 plotymargin 0.0 在指标顶部和底部留出的边距量（0.15 -\u0026gt; 15%）。有时 matplotlib 绘图会超出轴的顶部/底部，可能需要一个边距。 plotyticks [] 用于控制绘制的 y 轴刻度。如果传递一个空列表，将自动计算“y 刻度”。对于像随机指标这样的东西，设置为已知的行业标准可能有意义：[20.0, 50.0, 80.0]。某些指标提供 upperband 和 lowerband 等参数，实际上用于操纵 y 刻度。 plothlines [] 用于控制沿指标轴绘制的水平线。如果传递一个空列表，则不会绘制水平线。对于像随机指标这样的东西，绘制已知的行业标准线可能有意义：[20.0, 80.0]。某些指标提供 upperband 和 lowerband 等参数，实际上用于操纵水平线。 plotyhlines [] 用于同时控制 plotyticks 和 plothlines，使用一个单一参数。 plotforce False 如果由于某种原因你认为某个指标应该绘制但未绘制……请最后设置为 True。 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/08-indicators/01-using-indicators/","section":"教程","summary":"指标可以在平台中的两个地方使用：\n策略内部 其他指标内部 指标在操作中的使用 # 在策略中，指标总是在 __init__ 中实例化，在 next 中使用或检查指标值。一个重要原则是：在 __init__ 中声明的任何指标（或派生值）都会在调用 next 前预先计算。\n","title":"内置指标的使用方法","type":"docs"},{"content":"在 1.5.0 版本之前，backtrader 对时间管理采用直接方式，即使用数据源计算的日期时间。用户输入的参数（如 fromdate 或 sessionstart）也可传递给数据源。\n这种方法在回测静态数据时效果很好，可以假设日期时间在进入系统前已处理好。\n但 1.5.0 版本之后，backtrader 开始支持实时数据源，这就需要考虑日期时间管理。如果以下情况总是成立，则不需要这种管理：\n纽约的交易者交易 ES-Mini。两者时区均为 US/Eastern。 柏林的交易者交易 DAX 期货。两者时区均为 CET（Europe/Berlin）。 上述直接方式可以工作，因为柏林的交易者可以这样写：\nclass Strategy(bt.Strategy): def next(self): # DAX 期货在 CET 时间 08:00 开盘 if self.data.datetime.time() \u0026lt; datetime.time(8, 30): # 市场运行 30 分钟前不操作 return 但当同一个柏林交易者交易 ES-Mini 时，直接方式的问题就暴露了。由于夏令时（DST）转换时间不同（美国和欧洲的 DST 切换时间不一致），会导致一年中某些周的时间不同步。\n以下代码并不总是有效：\nclass Strategy(bt.Strategy): def next(self): # SPX 在 US/Eastern 时间全年 09:30 开盘 # 对应 CET 大部分时间是 15:30 # 但有时是 16:30 或 14:30，取决于美欧 DST 切换时间 # 因此以下代码不可靠 if self.data.datetime.time() \u0026lt; datetime.time(16, 0): # 市场运行 30 分钟前不操作 return 使用时区操作 # 为解决上述问题并保持兼容性，backtrader 提供了以下选项：\n日期时间输入 # 默认情况下，平台不会修改数据源提供的日期时间。\n用户可以通过以下方式覆盖输入：\n为数据源提供 tzinput 参数，该参数需兼容 datetime.tzinfo 接口，通常是一个 pytz.timezone 实例。 这样，backtrader 内部使用的时间被视为 UTC 类格式，即：\n数据源本来就以 UTC 格式存储。 经过 tzinput 转换后。 它并非真正的 UTC，但因为是用户的参考系，所以称为 UTC 类。\n日期时间输出 # 如果数据源能自动确定时区，默认使用自动确定的时区。\n这在实时数据源场景下尤其有意义。例如，柏林（CET）的交易者交易 US/Eastern 的产品：\n交易者始终看到正确的时间。在上例中，开盘时间在 US/Eastern 时区始终是 09:30，而不是一年中大部分时间的 15:30 CET，有时是 16:30 CET，有时是 14:30 CET。\n如果不能确定，则输出为输入时确定的时间（UTC 类时间）。\n用户可以通过以下方式覆盖输出时区：\n为数据源提供 tz 参数，兼容 datetime.tzinfo 接口，通常是一个 pytz.timezone 实例。 注意：\n用户输入的参数（如 fromdate 或 sessionstart）应与实际 tz 同步，无论 tz 是自动计算、用户提供还是默认（None 表示直接输入-输出）。\n综合以上内容，来看柏林的交易者交易 US/Eastern 时区的产品：\nimport pytz import bt data = bt.feeds.MyFeed(\u0026#39;ES-Mini\u0026#39;, tz=pytz.timezone(\u0026#39;US/Eastern\u0026#39;)) class Strategy(bt.Strategy): def next(self): # 这将全年有效。 # 数据源在 \u0026#39;US/Eastern\u0026#39; 时区框架内返回数据， # 以 \u0026#39;10:00\u0026#39; 作为参考时间。 # 因为 SPX 在 US/Eastern 时区始终是 09:30 开盘。 if self.data.datetime.time() \u0026lt; datetime.time(10, 0): # 市场运行 30 分钟前不操作 return 对于可以自动确定输出时区的数据源：\nimport bt data = bt.feeds.MyFeedAutoTZ(\u0026#39;ES-Mini\u0026#39;) class Strategy(bt.Strategy): def next(self): # 这将全年有效。 # 数据源在 \u0026#39;US/Eastern\u0026#39; 时区框架内返回数据， # 以 \u0026#39;10:00\u0026#39; 作为参考时间。 # 因为 SPX 在 US/Eastern 时区始终是 09:30 开盘。 if self.data.datetime.time() \u0026lt; datetime.time(10, 0): # 市场运行 30 分钟前不操作 return 上面示例中的 MyFeed 和 MyFeedAuto 仅为虚拟名称。\n注意：\n目前发行版中唯一能自动确定时区的数据源是连接到 Interactive Brokers 的数据源。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/17-datetime/01-management/","section":"教程","summary":"在 1.5.0 版本之前，backtrader 对时间管理采用直接方式，即使用数据源计算的日期时间。用户输入的参数（如 fromdate 或 sessionstart）也可传递给数据源。\n","title":"日期时间管理详解","type":"docs"},{"content":"Backtrader 提供了一组数据源解析器（目前都是基于 CSV 的），用于从不同来源加载数据。\nYahoo（在线或已保存到文件） VisualChart（参见 www.visualchart.com） Backtrader CSV（自定义格式用于测试） 通用 CSV 支持 如快速入门指南所示，将数据源添加到 Cerebro 实例后，可在策略中通过以下方式访问：\nself.datas 数组（按插入顺序） 别名： self.data 和 self.data0 指向第一个元素 self.dataX 指向数组中索引为 X 的元素 快速提醒：\nimport backtrader as bt import backtrader.feeds as btfeeds data = btfeeds.YahooFinanceCSVData(dataname=\u0026#39;wheremydatacsvis.csv\u0026#39;) cerebro = bt.Cerebro() cerebro.adddata(data) # 可以传递一个 \u0026#39;name\u0026#39; 参数用于绘图 数据源通用参数 # 该数据源可直接从 Yahoo 下载数据并输入系统。\nBacktrader 数据源常用参数：\n参数名 默认值 描述 dataname None 必须提供。其含义因数据源类型而异，例如文件路径、股票代码等。 name '' 用于绘图时的装饰性名称。如果未指定，可能会从 dataname 派生（例如：文件路径的最后一部分）。 fromdate mindate Python datetime 对象，表示应忽略此日期之前的任何数据。 todate maxdate Python datetime 对象，表示应忽略此日期之后的任何数据。 timeframe TimeFrame.Days 时间框架。可能的值包括：Ticks、Seconds、Minutes、Days、Weeks、Months 和 Years。 compression 1 每个实际条形图的条形数。仅在数据重采样/重放中有效。 sessionstart None 数据会话的开始时间。可用于重采样等目的。 sessionend None 数据会话的结束时间。可用于重采样等目的。 CSV 数据源通用参数 # 参数（除了通用参数外）：\n参数名 默认值 描述 headers True 指示传递的数据是否具有初始标题行。 separator ',' 分隔符，用于标记 CSV 每行中的各个字段。 GenericCSVData # 此类公开了一个通用接口，允许解析几乎所有的 CSV 文件格式。\n根据参数定义的顺序和字段存在情况解析 CSV 文件。\n特定参数（或特定含义）：\n参数名 默认值 描述 dataname 必须提供 要解析的文件名或类似文件的对象。 datetime 0 包含日期（或日期时间）字段的列索引。 time -1 如果与日期时间字段分开，则包含时间字段的列索引（-1 表示不存在）。 open 1 包含开盘价字段的列索引。 high 2 包含最高价字段的列索引。 low 3 包含最低价字段的列索引。 close 4 包含收盘价字段的列索引。 volume 5 包含成交量字段的列索引。 openinterest 6 包含未平仓合约数字段的列索引。 nullvalue float('NaN') 如果缺少应有的值（CSV 字段为空），将使用的值。 dtformat '%Y-%m-%d %H:%M:%S' 用于解析日期时间 CSV 字段的格式。 tmformat '%H:%M:%S' 如果存在，用于解析时间 CSV 字段的格式（默认情况下时间 CSV 字段不存在）。 这些参数让你根据 CSV 文件的结构自定义数据解析方式，确保正确加载数据。\n示例使用 # 满足以下要求的示例用法：\n限制输入年份为 2000 HLOC 顺序而不是 OHLC 将缺失值替换为零（0.0） 提供日线数据，日期时间只是格式为 YYYY-MM-DD 的日期 没有 openinterest 列 代码如下：\nimport datetime import backtrader as bt import backtrader.feeds as btfeeds ... ... data = btfeeds.GenericCSVData( dataname=\u0026#39;mydata.csv\u0026#39;, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), nullvalue=0.0, dtformat=(\u0026#39;%Y-%m-%d\u0026#39;), datetime=0, high=1, low=2, open=3, close=4, volume=5, openinterest=-1 ) ... 略微修改后的要求：\n限制输入年份为 2000 HLOC 顺序而不是 OHLC 将缺失值替换为零（0.0） 提供日内数据，带有单独的日期和时间列 日期格式为 YYYY-MM-DD 时间格式为 HH.MM.SS（而不是通常的 HH:MM:SS） 没有 openinterest 列 代码如下：\nimport datetime import backtrader as bt import backtrader.feeds as btfeeds ... ... data = btfeeds.GenericCSVData( dataname=\u0026#39;mydata.csv\u0026#39;, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), nullvalue=0.0, dtformat=(\u0026#39;%Y-%m-%d\u0026#39;), tmformat=(\u0026#39;%H.%M.%S\u0026#39;), datetime=0, time=1, high=2, low=3, open=4, close=5, volume=6, openinterest=-1 ) 也可以通过子类化永久保存：\nimport datetime import backtrader.feeds as btfeeds class MyHLOC(btfeeds.GenericCSVData): params = ( (\u0026#39;fromdate\u0026#39;, datetime.datetime(2000, 1, 1)), (\u0026#39;todate\u0026#39;, datetime.datetime(2000, 12, 31)), (\u0026#39;nullvalue\u0026#39;, 0.0), (\u0026#39;dtformat\u0026#39;, (\u0026#39;%Y-%m-%d\u0026#39;)), (\u0026#39;tmformat\u0026#39;, (\u0026#39;%H.%M.%S\u0026#39;)), (\u0026#39;datetime\u0026#39;, 0), (\u0026#39;time\u0026#39;, 1), (\u0026#39;high\u0026#39;, 2), (\u0026#39;low\u0026#39;, 3), (\u0026#39;open\u0026#39;, 4), (\u0026#39;close\u0026#39;, 5), (\u0026#39;volume\u0026#39;, 6), (\u0026#39;openinterest\u0026#39;, -1) ) 现在只需提供 dataname 即可使用此类：\ndata = btfeeds.MyHLOC(dataname=\u0026#39;mydata.csv\u0026#39;) ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/01-datafeeds/","section":"教程","summary":"Backtrader 提供了一组数据源解析器（目前都是基于 CSV 的），用于从不同来源加载数据。\nYahoo（在线或已保存到文件） VisualChart（参见 www.visualchart.com） Backtrader CSV（自定义格式用于测试） 通用 CSV 支持 如快速入门指南所示，将数据源添加到 Cerebro 实例后，可在策略中通过以下方式访问：\n","title":"数据源 DataFeed 详解","type":"docs"},{"content":" 中立性 # backtrader 对数据所代表的内容保持中立。不同的佣金方案可以应用于同一数据集。下面来看看如何实现。\n经纪商快捷方式 # 这种方式让最终用户不用直接操作 CommissionInfo 对象，只需一次函数调用即可创建/设置佣金方案。在常规的 cerebro 创建/设置过程中，在经纪商属性上调用 setcommission 即可。\n以下调用设置了使用 Interactive Brokers 操作 Eurostoxx50 期货的常规佣金方案：\ncerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0) 由于大多数用户通常只测试单个工具，这已经足够了。\n如果你为数据馈送命名（当图表需要同时处理多个工具时），这个调用可以扩展如下：\ncerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0, name=\u0026#39;Eurostoxxx50\u0026#39;) 这样，该佣金方案将仅应用于名称匹配 Eurostoxx50 的工具。\nsetcommission 参数的含义 # commission（默认值：0.0）\n每次操作的货币单位，绝对值或百分比。上述示例中，每份合约的买入和卖出费用为 2.0 欧元。 关键是要理解何时使用绝对值或百分比。\n如果 margin 为 False（即 False、0 或 None），佣金按价格乘以数量的百分比计算。\n如果 margin 是其他值，操作视为期货类工具，佣金为每张合约的固定价格。\nmargin（默认值：None）\n操作期货类工具时需要的保证金。如上所述：\n如果未设置 margin，佣金按百分比计算，应用于买卖操作的价格 x 数量。\n如果设置了 margin，佣金按固定值计算，乘以买卖操作的数量。\nmult（默认值：1.0）\n对于期货类工具，此值决定应用于损益计算的乘数，使期货同时具有吸引力和风险。\nname（默认值：None）\n将佣金方案应用于名称匹配的工具。可以在创建数据馈送时设置此值。如果未设置，则方案将适用于系统中的任何数据。\n两个示例：股票 vs 期货 # 期货的示例：\ncerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0) 股票的示例：\ncerebro.broker.setcommission(commission=0.005) # 交易金额的 0.5% 注意\n第二种语法不设置 margin 和 mult，backtrader 会智能地将佣金视为百分比处理。\n要完全指定佣金方案，需要创建 CommissionInfo 的子类。\n创建永久性佣金方案 # 可以通过直接使用 CommissionInfo 类来创建更持久的佣金方案。用户可以这样定义：\nimport backtrader as bt commEurostoxx50 = bt.CommissionInfo(commission=2.0, margin=2000.0, mult=10.0) 然后在另一个 Python 模块中应用：\nfrom mycomm import commEurostoxx50 ... cerebro.broker.addcommissioninfo(commEuroStoxx50, name=\u0026#39;Eurostoxxx50\u0026#39;) CommissionInfo 是一个对象，使用与 backtrader 其他对象类似的 params 声明。因此上述内容也可以写成：\nimport backtrader as bt class CommEurostoxx50(bt.CommissionInfo): params = dict(commission=2.0, margin=2000.0, mult=10.0) 然后：\nfrom mycomm import CommEurostoxx50 ... cerebro.broker.addcommissioninfo(CommEuroStoxx50(), name=\u0026#39;Eurostoxxx50\u0026#39;) 使用 SMA 交叉的实际比较 # 使用简单移动平均交叉作为进出信号，在同一数据集上测试期货类和股票类佣金方案。\n注意\n期货头寸不仅可以执行进出操作，还可以在每次机会时进行反转。但这个示例的重点是比较佣金方案。\n代码（见底部完整策略）是相同的，可以在定义策略之前选择方案。\nfutures_like = True if futures_like: commission, margin, mult = 2.0, 2000.0, 10.0 else: commission, margin, mult = 0.005, None, 1 只需将 futures_like 设置为 false 即可使用股票类方案运行。\n日志代码用于评估不同佣金方案的影响。我们重点关注前两个操作。\n对于期货：\n2006-03-09, BUY CREATE, 3757.59 2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00 2006-04-11, SELL CREATE, 3788.81 2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00 2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00 2006-04-20, BUY CREATE, 3860.00 2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00 2006-04-28, SELL CREATE, 3839.90 2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00 2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30 对于股票：\n2006-03-09, BUY CREATE, 3757.59 2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77 2006-04-11, SELL CREATE, 3788.81 2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93 2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91 2006-04-20, BUY CREATE, 3860.00 2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32 2006-04-28, SELL CREATE, 3839.90 2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20 2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84 第一个操作的价格如下：\n买入（执行）：3754.13 卖出（执行）：3786.93 期货的损益（含佣金）：324.0\n股票的损益（含佣金）：-4.91\n佣金完全吞噬了股票操作的利润，而期货操作的利润只受到轻微影响。\n第二个操作：\n买入（执行）：3863.57 卖出（执行）：3389.24 期货的损益（含佣金）：-247.30\n股票的损益（含佣金）：-62.84\n对于这个亏损操作，期货的影响明显更大。\n但：\n期货累计净利润：324.00 + (-247.30) = 76.70\n股票累计净利润：(-4.91) + (-62.84) = -67.75\n下图中可以看到累计效果。全年结束时，期货产生了更大的利润，但也遭遇了更大的回撤（更深的水下）。\n但关键是：无论是期货还是股票……都可以进行回测。\n代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind futures_like = True if futures_like: commission, margin, mult = 2.0, 2000.0, 10.0 else: commission, margin, mult = 0. 005, None, 1 class SMACrossOver(bt.Strategy): def log(self, txt, dt=None): \u0026#39;\u0026#39;\u0026#39; Logging function for this strategy\u0026#39;\u0026#39;\u0026#39; dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def notify(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash if order.status in [order.Completed, order.Canceled, order.Margin]: if order.isbuy(): self.log( \u0026#39;BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm self.opsize = order.executed.size else: # Sell self.log(\u0026#39;SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) gross_pnl = (order.executed.price - self.buyprice) * \\ self.opsize if margin: gross_pnl *= mult net_pnl = gross_pnl - self.buycomm - order.executed.comm self.log(\u0026#39;OPERATION PROFIT, GROSS %.2f, NET %.2f\u0026#39; % (gross_pnl, net_pnl)) def __init__(self): sma = btind.SMA(self.data) # \u0026gt; 0 crossing up / \u0026lt; 0 crossing down self.buysell_sig = btind.CrossOver(self.data, sma) def next(self): if self.buysell_sig \u0026gt; 0: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.data.close[0]) self.buy() # keep order ref to avoid 2nd orders elif self.position and self.buysell_sig \u0026lt; 0: self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.data.close[0]) self.sell() if __name__ == \u0026#39;__main__\u0026#39;: # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(SMACrossOver) # Create a Data Feed datapath = (\u0026#39;../../datas/2006-day-001.txt\u0026#39;) data = bt.feeds.BacktraderCSVData(dataname=datapath) # Add the Data Feed to Cerebro cerebro.adddata(data) # set commission scheme -- CHANGE HERE TO PLAY cerebro.broker.setcommission( commission=commission, margin=margin, mult=mult) # Run over everything cerebro.run() # Plot the result cerebro.plot() 参考 # class backtrader.CommInfoBase() 基础类用于佣金方案。\n参数：\ncommission（默认值：0.0）：基础佣金值，百分比或货币单位 mult（默认值：1.0）：应用于资产价值/利润的乘数 margin（默认值：None）：开/持仓所需的货币单位，仅当类中的 _stocklike 属性设置为 False 时适用 automargin（默认值：False）：用于方法 get_margin，自动计算所需的保证金 commtype（默认值：None）：支持 CommInfoBase.COMM_PERC（百分比佣金）和 CommInfoBase.COMM_FIXED（固定佣金） 默认值 None 用于保留与旧 CommissionInfo 对象的兼容性。如果 commtype 设置为 None，则按以下规则处理：\nmargin 为 None：内部 _commtype 设为 COMM_PERC，_stocklike 设为 True（按百分比操作股票）\nmargin 不为 None：_commtype 设为 COMM_FIXED，_stocklike 设为 False（按固定佣金操作期货）\n如果此参数设置为其他值，则传递给内部 _commtype 属性，stocklike 参数和内部 _stocklike 属性同理。\nstocklike（默认值：False）：指示工具是类似股票还是类似期货（参见上述 commtype 说明）\npercabs（默认值：False）：当 commtype 设置为 COMM_PERC 时，参数 commission 是否理解为 XX% 或 0.XX\n如果为 True：0.XX\n如果为 False：XX%\ninterest（默认值：0.0）：如果非零，表示持有空头头寸的年利率。主要用于股票卖空。\n公式：天数 x 价格 x abs(大小) x (利息 / 365)\n必须以绝对值表示：0.05 -\u0026gt; 5%\n可以通过覆盖 _get_credit_interest 方法更改此行为。\ninterest_long（默认值：False）：某些产品（如 ETF）对空头和多头头寸都收取利息。如果为 True 且 interest 非零，则对两个方向都收取利息。\nleverage（默认值：1.0）：与所需现金相比的杠杆比例\nCommissionInfo 类 # 实际佣金方案的基类。CommInfoBase 是为了保持对 backtrader 原有不完整支持的兼容性。新的佣金方案从此类派生。默认的 percabs 值也更改为 True。\n参数：\npercabs（默认值：True）：当 commtype 设置为 COMM_PERC 时，参数 commission 是否理解为 XX% 或 0.XX\n如果为 True：0.XX\n如果为 False：XX%\n返回此佣金方案允许的杠杆水平。\nget_leverage() 返回在给定价格下满足现金操作所需的数量。\ngetsize(price, cash) 返回操作所需的现金金额。\ngetoperationcost(size, price) 返回给定价格下的数量价值。对于期货类对象，固定为 数量 x 保证金。\ngetvaluesize(size, price) 返回给定价格下的头寸价值。对于期货类对象，固定为数量 x 保证金。\ngetvalue(position, price) 返回在给定价格下单个资产所需的实际保证金。默认实现策略如下：\n如果 automargin 为 False，使用 margin 参数 如果 automargin \u0026lt; 0，使用 mult 参数，即 mult * price 如果 automargin \u0026gt; 0，使用 automargin 参数，即 automargin * price get_margin(price) 计算在给定价格下的操作佣金\ngetcommission(size, price) 计算在给定价格下的操作佣金\npseudoexec：如果为 True，表示操作尚未执行\n_getcommission(size, price, pseudoexec) 返回头寸的实际损益\nprofitandloss(size, price, newprice) 计算价格差异的现金调整\ncashadjust(size, price, newprice) 计算股票卖空或特定产品的利息费用。\nget_credit_interest(data, pos, dt) 此方法返回由经纪商收取的利息费用。\n在 size \u0026gt; 0 的情况下，仅当类的参数 interest_long 为 True 时才调用此方法。\n计算利息的公式是：天数 x 价格 x abs(大小) x (利息 / 365)。\n_get_credit_interest(data, size, price, days, dt0, dt1) 参数：\ndata：收取利息的数据馈送 size：当前头寸大小。\u0026gt; 0 表示多头头寸，\u0026lt; 0 表示空头头寸（此参数不会为 0） price：当前头寸价格 days：自上次利息计算以来经过的天数（即 (dt0 - dt1).days） dt0：（datetime.datetime）当前日期时间 dt1：（datetime.datetime）上次计算的日期时间 dt0 和 dt1 在默认实现中未使用，作为覆盖方法的额外输入提供。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/11-commission-schemes/01-commission-schemes/","section":"教程","summary":"中立性 # backtrader 对数据所代表的内容保持中立。不同的佣金方案可以应用于同一数据集。下面来看看如何实现。\n","title":"佣金方案详解","type":"docs"},{"content":"在 backtrader 中运行的策略主要处理数据源和指标。数据源添加到 Cerebro 实例中，最终成为策略的输入（解析后作为实例属性提供），而指标由策略本身声明和管理。\n到目前为止，所有 backtrader 示例图表都绘制了三样看似理所当然的东西，因为它们在任何地方都没有声明：\n现金和价值（经纪商中的资金情况） 交易（即操作） 买/卖订单 它们是观察器，存在于 backtrader.observers 子模块中。Cerebro 通过参数 stdstats（默认：True）自动将它们添加到策略中。\n如果使用默认设置，Cerebro 会执行以下等效代码：\nimport backtrader as bt cerebro = bt.Cerebro() # 默认参数：stdstats=True cerebro.addobserver(bt.observers.Broker) cerebro.addobserver(bt.observers.Trades) cerebro.addobserver(bt.observers.BuySell) 我们先查看带这三个默认观察器的图表（即使没有发出订单，没有交易发生，现金和投资组合价值也没有变化）：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt import backtrader.feeds as btfeeds if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro(stdstats=False) cerebro.addstrategy(bt.Strategy) data = bt.feeds.BacktraderCSVData(dataname=\u0026#39;../../datas/2006-day-001.txt\u0026#39;) cerebro.adddata(data) cerebro.run() cerebro.plot() 现在将 stdstats 设置为 False 来创建 Cerebro 实例（也可以在调用 run 时完成）：\ncerebro = bt.Cerebro(stdstats=False) 图表现在有所不同。\n访问观察器 # 如上所示，观察器默认已经存在并收集信息，这些信息可用于统计目的。你可以通过策略的 stats 属性访问观察器。\n这只是一个占位符。回顾上面添加默认观察器的代码：\ncerebro.addobserver(backtrader.observers.Broker) 显而易见的问题是：如何访问 Broker 观察器。下面是在策略的 next 方法中如何做到这一点的示例：\nclass MyStrategy(bt.Strategy): def next(self): if self.stats.broker.value[0] \u0026lt; 1000.0: print(\u0026#39;WHITE FLAG ... I LOST TOO MUCH\u0026#39;) elif self.stats.broker.value[0] \u0026gt; 10000000.0: print(\u0026#39;TIME FOR THE VIRGIN ISLANDS ....!!!\u0026#39;) Broker 观察器与数据、指标和策略本身一样，也是一个线条对象。它包含两条线：\ncash value 观察器的实现 # 实现方式与指标非常相似：\nclass Broker(Observer): alias = (\u0026#39;CashValue\u0026#39;,) lines = (\u0026#39;cash\u0026#39;, \u0026#39;value\u0026#39;) plotinfo = dict(plot=True, subplot=True) def next(self): self.lines.cash[0] = self._owner.broker.getcash() self.lines.value[0] = value = self._owner.broker.getvalue() 步骤：\n从 Observer（而不是 Indicator）派生 根据需要声明线条和参数（Broker 有两条线但没有参数） 将有一个自动属性 _owner，即持有观察器的策略 观察器在以下条件下开始运行：\n所有指标都已计算完毕 策略的 next 方法已执行完毕 这意味着在周期结束时，它们观察已发生的情况。在 Broker 的情况下，它只是记录每个时间点的经纪商现金和投资组合价值。\n将观察器添加到策略 # 如上所述，Cerebro 使用 stdstats 参数决定是否添加三个默认观察器，从而减轻最终用户的工作负担。\n还可以添加其他观察器，无论是与 stdstats 一起使用还是替换它们。\n让我们来看一个常规策略：当收盘价高于简单移动平均线时买入，低于时卖出。\n这里”添加”了一个观察器：\nDrawDown，backtrader 生态系统中已有的观察器 from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import os.path import time import sys import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class MyStrategy(bt.Strategy): params = ((\u0026#39;smaperiod\u0026#39;, 15),) def log(self, txt, dt=None): \u0026#39;\u0026#39;\u0026#39; 记录此策略的日志功能 \u0026#39;\u0026#39;\u0026#39; dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): # 主数据上的简单移动平均线 sma = btind.SMA(period=self.p.smaperiod) # 交叉（1：向上，-1：向下）收盘价/移动平均线 self.buysell = btind.CrossOver(self.data.close, sma, plot=True) # 哨兵为 None：允许新订单 self.order = None def next(self): # 访问 -1，因为 drawdown[0] 将在“next”之后计算 self.log(\u0026#39;DrawDown: %.2f\u0026#39; % self.stats.drawdown.drawdown[-1]) self.log(\u0026#39;MaxDrawDown: %.2f\u0026#39; % self.stats.drawdown.maxdrawdown[-1]) # 检查我们是否在市场中 if self.position: if self.buysell \u0026lt; 0: self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.data.close[0]) self.sell() elif self.buysell \u0026gt; 0: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.data.close[0]) self.buy() def runstrat(): cerebro = bt.Cerebro() data = bt.feeds.BacktraderCSVData(dataname=\u0026#39;../../datas/2006-day-001.txt\u0026#39;) cerebro.adddata(data) cerebro.addobserver(bt.observers.DrawDown) cerebro.addstrategy(MyStrategy) cerebro.run() cerebro.plot() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() 视觉输出显示了回撤的演变：\n部分文本输出：\n... 2006-12-14T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-15T23:59:59+00:00, DrawDown: 0.22 2006-12-15T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-18T23:59:59+00:00, DrawDown: 0.00 2006-12-18T23:59:59+00:00, MaxDrawDown: 2.62 ... 注意\n如文本输出和代码所示，DrawDown 观察器有两条线：\ndrawdown maxdrawdown 默认不绘制 maxdrawdown 线，但它仍然可供使用。\n实际上，maxdrawdown 的最后一个值也可以通过一个名称为 maxdd 的直接属性（非线条）获取。\n开发观察器 # 上面展示了 Broker 观察器的实现。要开发有意义的观察器，可以使用以下信息：\nself._owner 是当前正在执行的策略 因此策略中的任何内容对观察器都是可用的。\n策略中可能有用的默认内部属性：\nbroker：提供对策略创建订单的经纪商实例的访问。如在 Broker 中所见，通过 getcash 和 getvalue 方法收集现金和投资组合价值。 _orderspending：策略创建的订单列表，这些订单经纪商已通知策略。BuySell 观察器遍历此列表，查找已执行的订单（全部或部分），为给定时间点创建平均执行价格（索引 0）。 _tradespending：交易列表（已完成的买入/卖出或卖出/买入对），由订单编译而成。观察器也可以通过 self._owner.stats 路径访问其他观察器。 自定义 OrderObserver # 标准的 BuySell 观察器只关心已执行的操作。我们可以创建一个显示订单创建和过期状态的观察器。\n为了清晰显示，不会与价格一起绘制，而是在单独的轴上展示。\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import math import backtrader as bt class OrderObserver(bt.observer.Observer): lines = (\u0026#39;created\u0026#39;, \u0026#39;expired\u0026#39;,) plotinfo = dict(plot=True, subplot=True, plotlinelabels=True) plotlines = dict( created=dict(marker=\u0026#39;*\u0026#39;, markersize=8.0, color=\u0026#39;lime\u0026#39;, fillstyle=\u0026#39;full\u0026#39;), expired=dict(marker=\u0026#39;s\u0026#39;, markersize=8.0, color=\u0026#39;red\u0026#39;, fillstyle=\u0026#39;full\u0026#39;) ) def next(self): for order in self._owner._orderspending: if order.data is not self.data: continue if not order.isbuy(): continue # 只关心“买入”订单，因为策略中的卖出订单是市场订单并会立即执行 if order.status in [bt.Order.Accepted, bt.Order.Submitted]: self.lines.created[0] = order.created.price elif order.status in [bt.Order.Expired]: self.lines.expired[0] = order.created.price 自定义观察器只关心买入订单，因为这是一个只做多的策略。卖出订单是市价单，会立即执行。\n更新后的策略 # from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind from orderobserver import OrderObserver class MyStrategy(bt.Strategy): params = ( (\u0026#39;smaperiod\u0026#39;, 15), (\u0026#39;limitperc\u0026#39;, 1.0), (\u0026#39;valid\u0026#39;, 7), ) def log(self, txt, dt=None): \u0026#39;\u0026#39;\u0026#39; 记录此策略的日志功能 \u0026#39;\u0026#39;\u0026#39; dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # 买入/卖出订单已提交/接受至/由经纪人 - 无需执行任何操作 self.log(\u0026#39;ORDER ACCEPTED/SUBMITTED\u0026#39;, dt=order.created.dt) self.order = order return if order.status in [order.Expired]: self.log(\u0026#39;BUY EXPIRED\u0026#39;) elif order.status in [order.Completed]: if order.isbuy(): self.log( \u0026#39;BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) else: # 卖出 self.log(\u0026#39;SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) # 哨兵为 None：允许新订单 self.order = None def __init__(self): # 主数据上的简单移动平均线 sma = btind.SMA(period=self.p.smaperiod) # 交叉（1：向上，-1：向下）收盘价/移动平均线 self.buysell = btind.CrossOver(self.data.close, sma, plot=True) # 哨兵为 None：允许新订单 self.order = None def next(self): if self.order: # 待处理订单...不做任何事 return # 检查我们是否在市场中 if self.position: if self.buysell \u0026lt; 0: self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.data.close[0]) self.sell() elif self.buysell \u0026gt; 0: plimit = self.data.close[0] * (1.0 - self.p.limitperc / 100.0) valid = self.data.datetime.date(0) + datetime.timedelta(days=self.p.valid) self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % plimit) self.buy(exectype=bt.Order.Limit, price=plimit, valid=valid) def runstrat(): cerebro = bt.Cerebro() data = bt.feeds.BacktraderCSVData(dataname=\u0026#39;../../datas/2006-day-001.txt\u0026#39;) cerebro.adddata(data) cerebro.addobserver(OrderObserver) cerebro.addstrategy(MyStrategy) cerebro.run() cerebro.plot() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() 结果图表显示多个订单已过期，可以在新子图（红色方块）中看到。此外，还可以看到”创建”和”执行”之间的天数差异。\n保存/保持统计数据 # 截至目前，backtrader 尚未实现跟踪观察器值并存储到文件的机制。最好的方法是：\n在策略的 start 方法中打开文件 在策略的 next 方法中写入值 以 DrawDown 观察器为例，可以这样做：\nclass MyStrategy(bt.Strategy): def start(self): self.mystats = open(\u0026#39;mystats.csv\u0026#39;, \u0026#39;wb\u0026#39;) self.mystats.write(\u0026#39;datetime,drawdown, maxdrawdown\\n\u0026#39;) def next(self): self.mystats.write(self.data.datetime.date(0).strftime(\u0026#39;%Y-%m-%d\u0026#39;)) self.mystats.write(\u0026#39;,%.2f\u0026#39; % self.stats.drawdown.drawdown[-1]) self.mystats.write(\u0026#39;,%.2f\u0026#39; % self.stats.drawdown.maxdrawdown[-1]) self.mystats.write(\u0026#39;\\n\u0026#39;) 要保存索引 0 的值，可以在所有观察器处理完毕后，将自定义观察器作为系统的最后一个观察器添加，将值写入 CSV 文件。\n注意，Writer 功能可以自动执行此任务。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/13-observers/01-statistics/","section":"教程","summary":"在 backtrader 中运行的策略主要处理数据源和指标。数据源添加到 Cerebro 实例中，最终成为策略的输入（解析后作为实例属性提供），而指标由策略本身声明和管理。\n到目前为止，所有 backtrader 示例图表都绘制了三样看似理所当然的东西，因为它们在任何地方都没有声明：\n","title":"运行时统计观察器","type":"docs"},{"content":"迄今为止，所有 backtrader 示例都是从零创建 Python 模块，加载数据、策略、观察器，并设置现金和佣金方案。\n算法交易的目标是自动化。backtrader 作为回测和算法交易平台，其自动化是必然的需求。\n安装 backtrader 后，提供了两个入口点用于自动化大多数任务：\nbt-run-py：一个脚本。 btrun（可执行文件）：由 setuptools 在打包时创建，在 Windows 下不会出现”找不到路径/文件”的错误。 以下描述适用于这两个工具。\nbtrun 允许你：\n指定要加载的数据源 设置数据格式 指定日期范围 传递参数给 Cerebro 禁用标准观察器 这是一个遗留开关（在 Cerebro 参数实现之前）。如果通过 Cerebro 传递了标准观察器相关参数，此开关将忽略（对应 Cerebro 的 stdstats 参数） 从内置或 Python 模块加载观察器（如 DrawDown） 设置经纪商现金和佣金方案（佣金、保证金、倍数） 启用绘图，控制图表的数量和样式 添加参数化的 writer 核心功能：加载策略（内置或来自 Python 模块） 传递参数给加载的策略 请参阅下面的脚本用法。\n应用用户定义策略 # 考虑以下策略，它：\n简单加载一个简单移动平均线（默认周期为 15） 打印输出 存在于名为 mymod.py 的文件中 from __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt import backtrader.indicators as btind class MyTest(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 15),) def log(self, txt, dt=None): \u0026#39;\u0026#39;\u0026#39;策略的日志记录函数\u0026#39;\u0026#39;\u0026#39; dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): sma = btind.SMA(period=self.p.period) def next(self): ltxt = \u0026#39;%d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f\u0026#39; self.log(ltxt % (len(self), self.data.open[0], self.data.high[0], self.data.low[0], self.data.close[0], self.data.volume[0], self.data.openinterest[0])) 使用常规测试示例执行策略非常简单：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --strategy mymod.py 图表输出：\n控制台输出：\n2006-01-20T23:59:59+00:00, 15, 3593.16, 3612.37, 3550.80, 3550.80, 0.00, 0.00 2006-01-23T23:59:59+00:00, 16, 3550.24, 3550.24, 3515.07, 3544.31, 0.00, 0.00 2006-01-24T23:59:59+00:00, 17, 3544.78, 3553.16, 3526.37, 3532.68, 0.00, 0.00 2006-01-25T23:59:59+00:00, 18, 3532.72, 3578.00, 3532.72, 3578.00, 0.00, 0.00 ... 2006-12-22T23:59:59+00:00, 252, 4109.86, 4109.86, 4072.62, 4073.50, 0.00, 0.00 2006-12-27T23:59:59+00:00, 253, 4079.70, 4134.86, 4079.70, 4134.86, 0.00, 0.00 2006-12-28T23:59:59+00:00, 254, 4137.44, 4142.06, 4125.14, 4130.66, 0.00, 0.00 2006-12-29T23:59:59+00:00, 255, 4130.12, 4142.01, 4119.94, 4119.94, 0.00, 0.00 相同策略，但将参数 period 设置为 50 的命令行：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --plot \\ --strategy mymod.py:period=50 图表输出：\n注意：如果没有提供 .py 扩展名，btrun 会自动添加。\n使用内置策略 # backtrader 逐步内置了一些示例策略。与 btrun 一起提供了一个标准的简单移动平均交叉策略，名称为 SMA_CrossOver。\n参数 # fast（默认 10）：快速移动平均线的周期 slow（默认 30）：慢速移动平均线的周期 该策略在快速移动平均线上穿慢速移动平均线时买入，并在快速移动平均线下穿慢速移动平均线时卖出（仅在之前已买入的情况下）。\n代码如下：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt import backtrader.indicators as btind class SMA_CrossOver(bt.Strategy): params = ((\u0026#39;fast\u0026#39;, 10), (\u0026#39;slow\u0026#39;, 30)) def __init__(self): sma_fast = btind.SMA(period=self.p.fast) sma_slow = btind.SMA(period=self.p.slow) self.buysig = btind.CrossOver(sma_fast, sma_slow) def next(self): if self.position.size: if self.buysig \u0026lt; 0: self.sell() elif self.buysig \u0026gt; 0: self.buy() 标准执行：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --plot \\ --strategy :SMA_CrossOver 注意 :。标准的加载策略表示法为：module:strategy:kwargs。规则如下：\n如果指定了 module 和 strategy，则使用该策略。 如果指定了 module 但未指定 strategy，则使用模块中找到的第一个策略。 如果未指定 module，则“strategy”被视为 backtrader 包中的策略。 如果指定了 module 和/或 strategy，如果存在 kwargs，它们将传递给相应的策略。 注意：相同的表示法和规则适用于 --observer、--analyzer 和 --indicator 选项，显然适用于相应的对象类型。\n输出结果：\n最后一个示例，添加佣金方案、现金并更改参数：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --plot \\ --cash 20000 \\ --commission 2.0 \\ --mult 10 \\ --margin 2000 \\ --strategy :SMA_CrossOver:fast=5,slow=20 输出结果：\n我们已经回测了策略：\n更改移动平均线周期 设置新的起始现金 为类似期货的工具设置佣金方案 观察每个条的现金持续变化，因为现金根据类似期货工具的每日变化进行调整。\n使用无策略 # 这其实有些夸张——仍然会应用一个策略，但你可以省略策略类型，系统会自动添加默认的 backtrader.Strategy。\n分析器、观察器和指标将自动注入策略中。\n例如：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --cash 20000 \\ --commission 2.0 \\ --mult 10 \\ --margin 2000 \\ --nostdstats \\ --observer :Broker 这不会做很多事情，但可以实现以下目的：\n在后台添加一个默认的 backtrader.Strategy Cerebro 不会实例化常规的 stdstats 观察器（Broker、BuySell、Trades） 手动添加一个 Broker 观察器 如上所述，nostdstats 是一个遗留参数。更新版本的 btrun 可以将参数直接传递给 Cerebro。等效调用如下：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --cash 20000 \\ --commission 2.0 \\ --mult 10 \\ --margin 2000 \\ --cerebro stdstats=False \\ --observer :Broker 添加分析器 # btrun 还支持使用与选择内部/外部分析器相同的语法添加分析器。\n例如，对 2005-2006 年进行夏普比率分析：\nbtrun --csvformat btcsv \\ --data ../../datas/2005-2006-day-001.txt \\ --strategy :SMA_CrossOver \\ --analyzer :SharpeRatio 控制台输出为空。\n如需打印分析器结果，需指定：\n--pranalyzer：默认打印（除非分析器覆盖了相应方法） --ppranalyzer：使用 pprint 模块美化输出 注意：这两个打印选项在 writer 成为 backtrader 一部分之前就有。使用不带 CSV 输出的 writer 可实现相同效果（且输出更优）。\n扩展上述示例：\nbtrun --csvformat btcsv \\ --data ../../datas/2005-2006-day-001.txt \\ --strategy :SMA_CrossOver \\ --analyzer :SharpeRatio \\ --plot \\ --pranalyzer ==================== == Analyzers ==================== ########## sharperatio ########## {\u0026#39;sharperatio\u0026#39;: 11.647332609673256} 好策略！（实际上纯属运气，示例中没有考虑佣金）\n图表（仅显示分析器未在图表中绘制，因为分析器无法绘制，它们不是线条对象）：\n使用 writer 参数的相同示例：\nbtrun --csvformat btcsv \\ --data ../../datas/2005-2006-day-001.txt \\ --strategy :SMA_CrossOver \\ --analyzer :SharpeRatio \\ --plot \\ --writer =============================================================================== Cerebro: ----------------------------------------------------------------------------- - Datas: +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Data0: - Name: 2005-2006-day-001 - Timeframe: Days - Compression: 1 ----------------------------------------------------------------------------- - Strategies: +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - SMA_CrossOver: ************************************************************************* - Params: - fast: 10 - slow: 30 - _movav: SMA ************************************************************************* - Indicators: ....................................................................... - SMA: - Lines: sma ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Params: - period: 30 ....................................................................... - CrossOver: - Lines: crossover - Params: None ************************************************************************* - Observers: ....................................................................... - Broker: - Lines: cash, value - Params: None ....................................................................... - BuySell: - Lines: buy, sell - Params: None ....................................................................... - Trades: - Lines: pnlplus, pnlminus - Params: None ************************************************************************* - Analyzers: ....................................................................... - Value: - Begin: 10000.0 - End: 10496.68 ....................................................................... - SharpeRatio: - Params: None ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Analysis: - sharperatio: 11.6473326097 添加指标和观察器 # 与策略和分析器相同，btrun 还可以添加：\n指标 观察器 语法与上面添加 Broker 观察器时所见完全相同。\n让我们重复示例，添加一个随机指标、Broker 并查看图表（我们将更改一些参数）：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --nostdstats \\ --observer :Broker \\ --indicator :Stochastic:period_dslow=5 \\ --plot 图表：\n绘图控制 # 上面大多数示例都使用了以下选项：\n--plot：激活创建默认图表 更多控制可以通过向 --plot 选项添加 kwargs 实现。 例如，使用蜡烛图代替默认的 LineOnClose 样式： --plot style=\u0026quot;candle\u0026quot; 调用如下：\nbtrun --csvformat btcsv \\ --data ../../datas/2006-day-001.txt \\ --nostdstats \\ --observer :Broker \\ --indicator :Stochastic:period_dslow=5 \\ --plot style=\\\u0026#34;candle\\\u0026#34; 注意：示例在 bash shell 中运行，传递参数给脚本之前会去除引号，因此需要使用反斜杠转义 \\\u0026quot; 确保 candle 作为字符串传递。\n图表：\n脚本用法 # 直接从脚本：\n$ btrun --help usage: btrun-script.py [-h] --data DATA [--cerebro [kwargs]] [--nostdstats] [--format {yahoocsv_unreversed,vchart,vchartcsv,yahoo,mt4csv,ibdata,sierracsv,yahoocsv,btcsv,vcdata}] [--fromdate FROMDATE] [--todate TODATE] [--timeframe {microseconds,seconds,weeks,months,minutes,days,years}] [--compression COMPRESSION] [--resample RESAMPLE | --replay REPLAY] [--strategy module:name:kwargs] [--signal module:signaltype:name:kwargs] [--observer module:name:kwargs] [--analyzer module:name:kwargs] [--pranalyzer | --ppranalyzer] [--indicator module:name:kwargs] [--writer [kwargs]] [--cash CASH] [--commission COMMISSION] [--margin MARGIN] [--mult MULT] [--interest INTEREST] [--interest_long] [--slip_perc SLIP_PERC] [--slip_fixed SLIP_FIXED] [--slip_open] [--no-slip_match] [--slip_out] [--flush] [--plot [kwargs]] Backtrader Run Script optional arguments: -h, --help show this help message and exit --resample RESAMPLE, -rs RESAMPLE resample with timeframe:compression values --replay REPLAY, -rp REPLAY replay with timeframe:compression values --pranalyzer, -pralyzer Automatically print analyzers --ppranalyzer, -ppralyzer Automatically PRETTY print analyzers --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example: --plot style=\u0026#34;candle\u0026#34; (to plot candlesticks) Data options: --data DATA, -d DATA Data files to be added to the system Cerebro options: --cerebro [kwargs], -cer [kwargs] The argument can be specified with the following form: - kwargs Example: \u0026#34;preload=True\u0026#34; which set its to True The passed kwargs will be passed directly to the cerebro instance created for the execution The available kwargs to cerebro are: - preload (default: True) - runonce (default: True) - maxcpus (default: None) - stdstats (default: True) - live (default: False) - exactbars (default: False) - preload (default: True) - writer (default False) - oldbuysell (default False) - tradehistory (default False) --nostdstats Disable the standard statistics observers --format {yahoocsv_unreversed,vchart,vchartcsv,yahoo,mt4csv,ibdata,sierracsv,yahoocsv,btcsv,vcdata}, --csvformat {yahoocsv_unreversed,vchart,vchartcsv,yahoo,mt4csv,ibdata,sierracsv,yahoocsv,btcsv,vcdata}, -c {yahoocsv_unreversed,vchart,vchartcsv,yahoo,mt4csv,ibdata,sierracsv,yahoocsv,btcsv,vcdata} CSV Format --fromdate FROMDATE, -f FROMDATE Starting date in YYYY-MM-DD[THH:MM:SS] format --todate TODATE, -t TODATE Ending date in YYYY-MM-DD[THH:MM:SS] format --timeframe {microseconds,seconds,weeks,months,minutes,days,years}, -tf {microseconds,seconds,weeks,months,minutes,days,years} Ending date in YYYY-MM-DD[THH:MM:SS] format --compression COMPRESSION, -cp COMPRESSION Ending date in YYYY-MM-DD[THH:MM:SS] format Strategy options: --strategy module:name:kwargs, -st module:name:kwargs This option can be specified multiple times. The argument can be specified with the following form: - module:classname:kwargs Example: mymod:myclass:a=1,b=2 kwargs is optional If module is omitted then class name will be sought in the built-in strategies module. Such as in: - :name:kwargs or :name If name is omitted, then the 1st strategy found in the mod will be used. Such as in: - module or module::kwargs Signals: --signal module:signaltype:name:kwargs, -sig module:signaltype:name:kwargs This option can be specified multiple times. The argument can be specified with the following form: - signaltype:module:signaltype:classname:kwargs Example: longshort+mymod:myclass:a=1,b=2 signaltype may be ommited: longshort will be used Example: mymod:myclass:a=1,b=2 kwargs is optional signaltype will be uppercased to match the defintions fromt the backtrader.signal module If module is omitted then class name will be sought in the built-in signals module. Such as in: - LONGSHORT::name:kwargs or :name If name is omitted, then the 1st signal found in the mod will be used. Such as in: - module or module:::kwargs Observers and statistics: --observer module:name:kwargs, -ob module:name:kwargs This option can be specified multiple times. The argument can be specified with the following form: - module:classname:kwargs Example: mymod:myclass:a=1,b=2 kwargs is optional If module is omitted then class name will be sought in the built-in observers module. Such as in: - :name:kwargs or :name If name is omitted, then the 1st observer found in the will be used. Such as in: - module or module::kwargs Analyzers: --analyzer module:name:kwargs, -an module:name:kwargs This option can be specified multiple times. The argument can be specified with the following form: - module:classname:kwargs Example: mymod:myclass:a=1,b=2 kwargs is optional If module is omitted then class name will be sought in the built-in analyzers module. Such as in: - :name:kwargs or :name If name is omitted, then the 1st analyzer found in the will be used. Such as in: - module or module::kwargs Indicators: --indicator module:name:kwargs, -ind module:name:kwargs This option can be specified multiple times. The argument can be specified with the following form: - module:classname:kwargs Example: mymod:myclass:a=1,b=2 kwargs is optional If module is omitted then class name will be sought in the built-in analyzers module. Such as in: - :name:kwargs or :name If name is omitted, then the 1st analyzer found in the will be used. Such as in: - module or module::kwargs Writers: --writer [kwargs], -wr [kwargs] This option can be specified multiple times. The argument can be specified with the following form: - kwargs Example: a=1,b=2 kwargs is optional It creates a system wide writer which outputs run data Please see the documentation for the available kwargs Cash and Commission Scheme Args: --cash CASH, -cash CASH Cash to set to the broker --commission COMMISSION, -comm COMMISSION Commission value to set --margin MARGIN, -marg MARGIN Margin type to set --mult MULT, -mul MULT Multiplier to use --interest INTEREST Credit Interest rate to apply (0.0x) --interest_long Apply credit interest to long positions --slip_perc SLIP_PERC Enable slippage with a percentage value --slip_fixed SLIP_FIXED Enable slippage with a fixed point value --slip_open enable slippage for when matching opening prices --no-slip_match Disable slip_match, ie: matching capped at high-low if slippage goes over those limits --slip_out with slip_match enabled, match outside high-low --flush flush the output - useful under win32 systems ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/18-automated-running/01-article/","section":"教程","summary":"迄今为止，所有 backtrader 示例都是从零创建 Python 模块，加载数据、策略、观察器，并设置现金和佣金方案。\n算法交易的目标是自动化。backtrader 作为回测和算法交易平台，其自动化是必然的需求。\n","title":"自动运行与定时任务部署","type":"docs"},{"content":" 移动和调整大小 # 问：我如何将我的控件移动到不同的位置或调整其大小？\n答： Fyne 应用中元素的位置和大小由它们所在的容器的布局控制。如果你的 UI 元素太小，请考虑使用不同的布局或容器。\n一个新的Window会扩展传递给SetContent()的任何元素以填满它的大小。每次你向此添加一个容器时，它都会根据布局划分可用空间。像HBox和VBox这样的布局会将内容缩小到其MinSize()以在一个或另一个维度上打包内容。像Max或Border这样的布局会扩展内容以填充空间。通过编写自定义布局，你可以完全控制容器中的项目。\n问：我的图像为什么这么小？\n答： 使用像 Fyne 这样的完全可缩放的用户界面工具包的一个困难是，坐标系统是与设备无关的。这允许应用根据连接的硬件以正确的分辨率或像素密度绘制，以获得最佳结果。这对于基于像素的图像意味着它们的大小可能会根据编译时未知的细节而变化。\n由于这种复杂性，使用canvas.NewImageFromFile()或类似调用加载的图像将不会设置大小，导致它非常小或默认情况下看起来被隐藏。当放置在适当的布局中时，图像将根据其FillMode属性进行拉伸。如果你希望图像始终设置为特定大小（或更大），你可以调用Image.SetMinSize()并为图像指定一个与设备无关的大小。\n容器和布局 # 问：我如何手动控制元素的位置\n答： 在某些情况下，你可能希望完全控制容器中元素的位置和大小。为此，你创建一个没有布局的容器。container.NewWithoutLayout()函数将创建一个用于手动定位的容器——你应该将你想要在这个容器中管理的图形元素列表传递给该构造函数。\n设置好后，你可以使用Move()和Resize()在每个元素上进行定位。在执行此操作时，请注意，它不会随着可用空间的变化而调整——它也没有明确的最小尺寸。要添加这些功能中的任何一个，你应该用自定义布局替换你的手动定位。\n","date":"2025-05-26","externalUrl":null,"permalink":"/docs/gofyne/10-faq/01-layout/","section":"教程","summary":"移动和调整大小 # 问：我如何将我的控件移动到不同的位置或调整其大小？\n答： Fyne 应用中元素的位置和大小由它们所在的容器的布局控制。如果你的 UI 元素太小，请考虑使用不同的布局或容器。\n","title":"布局与控件大小","type":"docs"},{"content":"Fyne 应用基于每个窗口有一个画布。每个画布有一个根 CanvasObject，它可以是一个单独的控件或一个容器，用于控制多个子对象的大小和位置，这些子对象由布局控制。\n位置 # 每个画布的原点位于左上角（0, 0），UI 的每个元素都可能根据输出设备进行缩放，因此 API 不描述像素或精确的尺寸。 例如，在120DPI的显示器上，位置（10, 10）可能从原点向右和向下各10像素，但在HiDPI（或“Retina”）显示器上，这可能更接近20像素。\n每个 CanvasObject 引用的位置都是相对于它的父级的。这对于布局算法很重要，但对于开发者在例如 Tappable.Tapped(PointEvent) 处理程序这样的情况下也很重要。这里的 X/Y 坐标将从按钮的左上角而不是整个画布计算。这样设计是为了让代码尽可能自包含。\n像素大小 # 像其他基于矢量的 GUI 库一样，Fyne 坐标需要基于某种基线显示器分辨率。所有缩放都是相对于这个值的。对于 Fyne 来说，该分辨率是120DPI。这意味着当你的显示器是120DPI且所有缩放值都设置为1时，fyne.Size中引用的尺寸将是1=1px。对于 HiDPI 屏幕，如上所述，实际 DPI 可能更接近240，在移动设备上甚至可能是360或更高。为了管理这种复杂性，工具包在内部管理缩放，因此你的应用总是看起来大小合适。如果用户设置的缩放比例较小，那么他们的应用将始终具有小于正常的字体、按钮等，当他们指定较大时，你的应用将适当放大。\n与 Material Design 相比，我们可以看到他们的基线 DPI 是 160，尽管数学上相似，但实际数字会有所不同。这意味着 Fyne 中的设备独立尺寸使用较小的数字来代表相同的物理尺寸。例如，Fyne 中高度为 18 的图标在标准的 Material Design（例如 Android）应用中的尺寸为 24。构建应用程序时，这并不重要，但在与设计师或熟悉 Material Design 的专家合作时可能很重要。\n如果你开始加载位图图像，像素尺寸将变得重要。通常这些图像会适当缩放，但如果你指定 FillMode=fyne.FillOriginal，则由于像素密度的不同，实际图像大小在不同设备上会有所不同。通常这个功能会在 Scroll 容器内使用。Fyne 还定义了一个 canvas.Raster 原始类型，它将在输出设备的像素密度下精确绘制像素。这使你的代码能够在不了解运行设备的详细信息的情况下，以最高可能的输出分辨率进行绘制。如果由于某种原因你需要“像素完美”定位，你需要将 CanvasObject.Size() 乘以 Canvas.Scale()。\n","date":"2025-05-11","externalUrl":null,"permalink":"/docs/gofyne/09-architecture/01-geometry/","section":"教程","summary":"Fyne 应用基于每个窗口有一个画布。每个画布有一个根 CanvasObject，它可以是一个单独的控件或一个容器，用于控制多个子对象的大小和位置，这些子对象由布局控制。\n","title":"几何 Geometry","type":"docs"},{"content":"在Fyne应用程序中，每个Container都使用一个简单的布局算法来排列其子元素。Fyne在fyne.io/fyne/v2/layout包中定义了许多可用的布局。如果你查看代码，你会看到它们都实现了Layout接口。\ntype Layout interface { Layout([]CanvasObject, Size) MinSize(objects []CanvasObject) Size } 任何应用程序都可以提供一个自定义布局来以非标准的方式排列控件。为此，你需要在自己的代码中实现上述接口。为了说明这一点，我们将创建一个新的布局，该布局将元素排列在对角线上，并排列到其容器的左下角。\n首先，我们将定义一个新类型diagonal，并定义其最小大小将是多少。为了计算这个，我们只需添加所有子元素（指定为MinSize的[]fyne.CanvasObject参数）的宽度和高度。\nimport \u0026#34;fyne.io/fyne/v2\u0026#34; type diagonal struct { } func (d *diagonal) MinSize(objects []fyne.CanvasObject) fyne.Size { w, h := float32(0), float32(0) for _, o := range objects { childSize := o.MinSize() w += childSize.Width h += childSize.Height } return fyne.NewSize(w, h) } 对这个类型，我们添加一个Layout()函数，该函数应该将所有指定的对象移动到第二个参数中指定的fyne.Size中。\n在我们的实现中，我们计算控件的左上角位置（这是0 x参数，并且有一个y位置，即容器的高度减去所有子项高度的总和）。从顶部位置开始，我们简单地将每个项目位置按前一个子项目的大小向前移动。\nfunc (d *diagonal) Layout(objects []fyne.CanvasObject, containerSize fyne.Size) { pos := fyne.NewPos(0, containerSize.Height - d.MinSize(objects).Height) for _, o := range objects { size := o.MinSize() o.Resize(size) o.Move(pos) pos = pos.Add(fyne.NewPos(size.Width, size.Height)) } } 创建自定义布局就是这么简单。现在代码都写好了，我们可以将其作为container.New的layout参数使用。下面的代码设置了3个Label控件，并将它们与我们新的布局放在一个容器中。如果你运行这个应用程序，你将看到对角线控件的排列，并且在调整窗口大小时，它们将对齐到可用空间的左下角。\npackage main import ( \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;Diagonal\u0026#34;) text1 := widget.NewLabel(\u0026#34;topleft\u0026#34;) text2 := widget.NewLabel(\u0026#34;Middle Label\u0026#34;) text3 := widget.NewLabel(\u0026#34;bottomright\u0026#34;) w.SetContent(container.New(\u0026amp;diagonal{}, text1, text2, text3)) w.ShowAndRun() } ","date":"2025-04-20","externalUrl":null,"permalink":"/docs/gofyne/08-extend/01-custom-layout/","section":"教程","summary":"在Fyne应用程序中，每个Container都使用一个简单的布局算法来排列其子元素。Fyne在fyne.io/fyne/v2/layout包中定义了许多可用的布局。如果你查看代码，你会看到它们都实现了Layout接口。\n","title":"自定义布局 Layout","type":"docs"},{"content":"数据绑定是 Fyne 工具包的一个强大新功能，它在版本 v2.0.0 中引入。通过使用数据绑定，我们可以避免手动管理许多标准对象，如 Label、Button 和 List。\n内置绑定支持许多原始类型（如 Int、String、Float 等）、列表（例如 StringList、BoolList）以及 Map 和 Struct 绑定。每种类型都可以使用简单的构造函数创建。例如，要使用零值创建一个新的字符串绑定，可以使用 binding.NewString()。你可以使用 Get 和 Set 方法获取或设置大多数数据绑定的值。\n也可以使用以 Bind 开头的类似函数绑定到现有值，它们都接受指向绑定类型的指针。要绑定到现有的 int，我们可以使用 binding.BindInt(\u0026amp;myInt)。通过保留对绑定值的引用而不是原始变量，我们可以配置控件和函数以自动响应任何更改。如果你直接更改外部数据，请确保调用 Reload() 以确保绑定系统读取新值。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;fyne.io/fyne/v2/data/binding\u0026#34; ) func main() { boundString := binding.NewString() s, _ := boundString.Get() log.Printf(\u0026#34;Bound = \u0026#39;%s\u0026#39;\u0026#34;, s) myInt := 5 boundInt := binding.BindInt(\u0026amp;myInt) i, _ := boundInt.Get() log.Printf(\u0026#34;Source = %d, bound = %d\u0026#34;, myInt, i) } 接下来我们开始学习关于简单绑定控件绑定。\n","date":"2025-04-02","externalUrl":null,"permalink":"/docs/gofyne/07-binding/00-overview/","section":"教程","summary":"数据绑定是 Fyne 工具包的一个强大新功能，它在版本 v2.0.0 中引入。通过使用数据绑定，我们可以避免手动管理许多标准对象，如 Label、Button 和 List。\n内置绑定支持许多原始类型（如 Int、String、Float 等）、列表（例如 StringList、BoolList）以及 Map 和 Struct 绑定。每种类型都可以使用简单的构造函数创建。例如，要使用零值创建一个新的字符串绑定，可以使用 binding.NewString()。你可以使用 Get 和 Set 方法获取或设置大多数数据绑定的值。\n","title":"数据绑定","type":"docs"},{"content":"List 集合控件是工具包中的集合控件之一。这些控件旨在帮助构建在呈现大量数据时性能非常高的界面。你还可以看到具有类似 API 的 Table 和 Tree 控件。由于这种设计，它们使用起来稍微复杂一些。\nList 使用回调函数来在需要数据时请求数据。有三个主要的回调函数：Length、CreateItem 和 UpdateItem。Length 回调（首先传递）是最简单的，它返回要展示的数据中有多少项。其他回调与模板相关 - 如何创建、缓存和重用图形元素。\nCreateItem 回调返回一个新的模板对象。当控件呈现时，这将使用真实数据重新使用。此对象的 MinSize 将影响 List 的最小尺寸。最后，UpdateItem 被调用来将一个数据项应用于缓存的模板。使用这个来设置准备显示的内容。\npackage main import ( \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) var data = []string{\u0026#34;a\u0026#34;, \u0026#34;string\u0026#34;, \u0026#34;list\u0026#34;} func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;列表控件\u0026#34;) list := widget.NewList( func() int { return len(data) }, func() fyne.CanvasObject { return widget.NewLabel(\u0026#34;template\u0026#34;) }, func(i widget.ListItemID, o fyne.CanvasObject) { o.(*widget.Label).SetText(data[i]) }) myWindow.SetContent(list) myWindow.ShowAndRun() } ","date":"2025-03-21","externalUrl":null,"permalink":"/docs/gofyne/06-collection/01-list/","section":"教程","summary":"List 集合控件是工具包中的集合控件之一。这些控件旨在帮助构建在呈现大量数据时性能非常高的界面。你还可以看到具有类似 API 的 Table 和 Tree 控件。由于这种设计，它们使用起来稍微复杂一些。\n","title":"列表 List","type":"docs"},{"content":"Widgets 是 Fyne 应用程序 GUI 的主要组件，它们可以被用在任何一个基本的 fyne.CanvasObject 可以使用的地方。它们管理用户交互，并且总是与当前主题相匹配。\nLabel widget 是最简单的一个 - 它向用户展示文本。与 canvas.Text 不同，它可以处理一些简单的格式化（如 \\n）和换行（通过设置 Wrapping 字段）。 你可以通过调用 widget.NewLabel(\u0026quot;some text\u0026quot;) 来创建一个标签，结果可以被赋值给一个变量或直接传递给一个容器。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Label Widget\u0026#34;) content := widget.NewLabel(\u0026#34;text\u0026#34;) myWindow.SetContent(content) myWindow.ShowAndRun() } ","date":"2025-02-25","externalUrl":null,"permalink":"/docs/gofyne/05-widget/01-label/","section":"教程","summary":"Widgets 是 Fyne 应用程序 GUI 的主要组件，它们可以被用在任何一个基本的 fyne.CanvasObject 可以使用的地方。它们管理用户交互，并且总是与当前主题相匹配。\nLabel widget 是最简单的一个 - 它向用户展示文本。与 canvas.Text 不同，它可以处理一些简单的格式化（如 \\n）和换行（通过设置 Wrapping 字段）。 你可以通过调用 widget.NewLabel(\"some text\") 来创建一个标签，结果可以被赋值给一个变量或直接传递给一个容器。\n","title":"标签 Label ","type":"docs"},{"content":"如在容器和布局中讨论的，容器中的元素可以使用布局来排列。本节探讨内置布局及其使用方法。\n最常用的布局是 layout.BoxLayout，它有两个变体，水平和垂直。盒子布局将所有元素排列在单个行或列中，并可选择间隔以协助对齐。\n水平盒子布局，通过 layout.NewHBoxLayout() 创建，将项目排列在单行中。盒子中的每个项目的宽度将设置为其 MinSize().Width，高度对所有项目而言都是相等的，值为所有 MinSize().Height 值中的最大值。该布局可用于容器中，或者你可以使用盒子控件 widget.NewHBox()。\n垂直盒子布局与之相似，但它将项目排列在一列中。每个项目的高度将设置为最小值，所有宽度将相等，设置为最小宽度中的最大值。\n为了在元素之间创建扩展空间（例如，使某些元素左对齐，其他元素右对齐），在项目中添加一个 layout.NewSpacer()。间隔符将扩展以填充所有可用空间。在垂直盒子布局开始处添加一个间隔符将导致所有项目底部对齐。你可以在水平排列的开始和结束处各添加一个间隔符，以创建居中对齐。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/layout\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;盒子布局\u0026#34;) text1 := canvas.NewText(\u0026#34;你好\u0026#34;, color.White) text2 := canvas.NewText(\u0026#34;在那里\u0026#34;, color.White) text3 := canvas.NewText(\u0026#34;(右侧)\u0026#34;, color.White) content := container.New(layout.NewHBoxLayout(), text1, text2, layout.NewSpacer(), text3) text4 := canvas.NewText(\u0026#34;居中\u0026#34;, color.White) centered := container.New(layout.NewHBoxLayout(), layout.NewSpacer(), text4, layout.NewSpacer()) myWindow.SetContent(container.New(layout.NewVBoxLayout(), content, centered)) myWindow.ShowAndRun() } ","date":"2025-01-29","externalUrl":null,"permalink":"/docs/gofyne/04-container/01-box/","section":"教程","summary":"如在容器和布局中讨论的，容器中的元素可以使用布局来排列。本节探讨内置布局及其使用方法。\n最常用的布局是 layout.BoxLayout，它有两个变体，水平和垂直。盒子布局将所有元素排列在单个行或列中，并可选择间隔以协助对齐。\n","title":"盒子 Box","type":"docs"},{"content":"canvas.Rectangle 是 Fyne 中最简单的画布对象。它显示指定颜色的区块。您也可以使用 FillColor 字段设置颜色。\n在这个示例中，矩形填充了窗口，因为它是唯一的内容元素。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;矩形\u0026#34;) rect := canvas.NewRectangle(color.White) w.SetContent(rect) w.Resize(fyne.NewSize(150, 100)) w.ShowAndRun() } 其他的 fyne.CanvasObject 类型有更多的配置，让我们接下来看看 canvas.Text。\n","date":"2025-01-02","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/01-rectangle/","section":"教程","summary":"canvas.Rectangle 是 Fyne 中最简单的画布对象。它显示指定颜色的区块。您也可以使用 FillColor 字段设置颜色。\n在这个示例中，矩形填充了窗口，因为它是唯一的内容元素。\npackage main import ( \"image/color\" \"fyne.io/fyne/v2\" \"fyne.io/fyne/v2/app\" \"fyne.io/fyne/v2/canvas\" ) func main() { myApp := app.New() w := myApp.NewWindow(\"矩形\") rect := canvas.NewRectangle(color.White) w.SetContent(rect) w.Resize(fyne.NewSize(150, 100)) w.ShowAndRun() } 其他的 fyne.CanvasObject 类型有更多的配置，让我们接下来看看 canvas.Text。\n","title":"矩形 Rectangle","type":"docs"},{"content":"在Fyne中，画布（Canvas）是应用程序绘制的区域。每个窗口都有一个画布，你可以通过Window.Canvas()访问它，但通常你会发现Window上的函数可以避免直接访问画布。\nFyne中可以绘制的所有内容都是CanvasObject类型。这个示例打开了一个新窗口，然后通过设置窗口画布的内容来展示不同类型的基本图形元素。如文本和圆形示例所示，每种类型的对象都有许多定制方式。\n除了使用Canvas.SetContent()更改显示的内容外，还可以更改当前可见的内容。例如，如果你更改了矩形的FillColour，可以使用rect.Refresh()请求刷新这个已存在的组件。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;time\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Canvas\u0026#34;) myCanvas := myWindow.Canvas() blue := color.NRGBA{R: 0, G: 0, B: 180, A: 255} rect := canvas.NewRectangle(blue) myCanvas.SetContent(rect) go func() { time.Sleep(time.Second) green := color.NRGBA{R: 0, G: 180, B: 0, A: 255} rect.FillColor = green rect.Refresh() }() myWindow.Resize(fyne.NewSize(100, 100)) myWindow.ShowAndRun() } 我们可以用相同的方式绘制许多不同的绘图元素，如圆形和文本。\nfunc setContentToText(c fyne.Canvas) { green := color.NRGBA{R: 0, G: 180, B: 0, A: 255} text := canvas.NewText(\u0026#34;Text\u0026#34;, green) text.TextStyle.Bold = true c.SetContent(text) } func setContentToCircle(c fyne.Canvas) { red := color.NRGBA{R: 0xff, G: 0x33, B: 0x33, A: 0xff} circle := canvas.NewCircle(color.White) circle.StrokeWidth = 4 circle.StrokeColor = red c.SetContent(circle) } 控件 Widget # fyne.Widget是一种特殊类型的画布对象，它与交互元素关联。在控件中，逻辑与其外观（也称为WidgetRenderer）是分开的。\n控件也是CanvasObject类型，因此我们可以将窗口的内容设置为单个控件。看看我们如何创建一个新的widget.Entry并将其设置为窗口的内容。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Widget\u0026#34;) myWindow.SetContent(widget.NewEntry()) myWindow.ShowAndRun() } 这段描述说明了如何在Fyne应用程序中处理和更新画布内容，以及如何使用控件来创建交互式用户界面元素。\n","date":"2024-11-27","externalUrl":null,"permalink":"/docs/gofyne/02-explore/01-canvas/","section":"教程","summary":"在Fyne中，画布（Canvas）是应用程序绘制的区域。每个窗口都有一个画布，你可以通过Window.Canvas()访问它，但通常你会发现Window上的函数可以避免直接访问画布。\n","title":"Canvas 和 CanvasObject","type":"docs"},{"content":"使用 Fyne 工具包构建跨平台应用程序非常简单，但在开始之前需要安装一些工具。如果您的计算机已经为Go开发设置好了，以下步骤可能不是必需的，但我们建议您阅读一下您操作系统的提示，以防万一。如果本教程后面的步骤出现问题，那么您应该重新检查以下的先决条件。\n先决条件 # Fyne需要3个基本元素：Go工具（至少版本1.12）、一个C编译器（用于与系统图形驱动连接）和一个系统图形驱动。根据您的操作系统，安装指令会有所不同，请选择下面适合您的选项卡查看安装指令。\n请注意，这些步骤仅用于开发 - 您的Fyne应用不需要为最终用户安装任何设置或依赖！\n请根据您的操作系统选择下面的说明进行安装。\nWindows # 从下载页面下载Go并按照说明安装 为Windows安装以下测试过的C编译器之一： MSYS2配合MingW-w64 - msys2.org TDM-GCC - tdm-gcc.tdragon.net Cygwin - cygwin.com 在Windows上，您的图形驱动程序已经安装好了，但建议确保它们是最新的。 使用MSYS2（推荐）的安装步骤如下：\n从msys2.org安装MSYS2 安装完成后不要使用打开的MSYS终端 从开始菜单打开\u0026quot;MSYS2 MinGW 64位\u0026quot; 执行以下命令（如果询问安装选项，请确保选择\u0026quot;全部\u0026quot;）： $ pacman -Syu $ pacman -S git mingw-w64-x86_64-toolchain 您需要将 /c/Program\\ Files/Go/bin和 ~/Go/bin 添加到您的 $PATH 中，对于MSYS2，您可以将以下命令粘贴到终端中： $ echo \u0026#34;export PATH=\\$PATH:/c/Program\\ Files/Go/bin:~/Go/bin\u0026#34; \u0026gt;\u0026gt; ~/.bashrc 为了让编译器在其他终端上工作，您需要设置Windows的%PATH%变量，以便找到这些工具。进入\u0026quot;编辑系统环境变量\u0026quot;控制面板，点击\u0026quot;高级\u0026quot;，并将\u0026quot;C:\\msys64\\mingw64\\bin\u0026quot;添加到路径列表中。 MacOS # 从下载页面下载Go并按照说明进行安装。 从Mac App Store安装Xcode。 通过打开终端窗口并输入以下命令来设置Xcode命令行工具：xcode-select --install。 在macOS上，图形驱动程序将已经安装。 Linux # 您需要使用包管理器安装Go、gcc和图形库头文件，以下命令之一可能会起作用。 Debian / Ubuntu: sudo apt-get install golang gcc libgl1-mesa-dev xorg-dev Fedora: sudo dnf install golang gcc libXcursor-devel libXrandr-devel mesa-libGL-devel libXi-devel libXinerama-devel libXxf86vm-devel Arch Linux: sudo pacman -S go xorg-server-devel libxcursor libxrandr libxinerama libxi Solus: sudo eopkg it -c system.devel golang mesalib-devel libxrandr-devel libxcursor-devel libxi-devel libxinerama-devel openSUSE: sudo zypper install go gcc libXcursor-devel libXrandr-devel Mesa-libGL-devel libXi-devel libXinerama-devel libXxf86vm-devel Void Linux: sudo xbps-install -S go base-devel xorg-server-devel libXrandr-devel libXcursor-devel libXinerama-devel Alpine Linux: sudo apk add go gcc libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev linux-headers mesa-dev NixOS: nix-shell -p libGL pkg-config xorg.libX11.dev xorg.libXcursor xorg.libXi xorg.libXinerama xorg.libXrandr xorg.libXxf86vm Raspberry Pi # 您需要使用包管理器安装Go、gcc和图形库头文件。\nsudo apt-get install golang gcc libegl1-mesa-dev xorg-dev BSD # 您需要使用包管理器安装Go、gcc和图形库头文件。\nFreeBSD: sudo pkg install go gcc xorg pkgconf OpenBSD: sudo pkg_add go NetBSD: sudo pkgin install go pkgconf Android # 要为Android开发应用，您首先需要为您当前的电脑（Windows、macOS或Linux）安装工具。 完成后，您需要安装Android SDK和Android NDK——推荐的方法是安装Android Studio，然后转到Tools \u0026gt; SDK Manager，并从SDK Tools安装NDK（并行）包。 或者，您可以下载Standalone Android NDK，这是一种更精简的方法。解压缩文件夹，并将ANDROID_NDK_HOME环境变量指向它。 iOS # 要为iOS开发应用，您将需要访问一台按照上面macOS标签配置的苹果Mac电脑。 您还需要创建一个苹果开发者账户并注册开发者计划（需要支付费用）以获取在任何设备上运行应用所需的证书。 Termux # 在Android上编译Fyne应用，您将需要Android 9或以上版本。\n安装fdroid，然后从那里安装termux。 打开Termux并安装Go和Git：pkg install golang git。 从https://github.com/Lzhiyong/termux-ndk安装NDK和SDK到termux，并设置环境变量ANDROID_HOME和ANDROID_NDK_HOME。 下载 # 使用Go模块（Go 1.16及更高版本要求），在使用包之前需要设置模块。\nGo模块（适用于Go 1.16或更新版本） # 如果您不使用模块或您的模块已经初始化，可以跳过此步骤到下一个步骤。运行以下命令并将MODULE_NAME替换为您偏好的模块名（应在为您的应用程序专门创建的新文件夹中调用）。\n$ cd myapp $ go mod init MODULE_NAME 现在您需要下载Fyne模块和辅助工具。使用以下命令完成：\n$ go get fyne.io/fyne/v2@latest $ go install fyne.io/fyne/v2/cmd/fyne@latest 如果您不确定Go模块如何工作，考虑阅读[教程：创建一个Go模块]。\n旧版Go安装 # 使用旧版Go发布安装Fyne工具包和辅助工具，只需执行go get命令：\n$ go get fyne.io/fyne/v2 $ go get fyne.io/fyne/v2/cmd/fyne 检查您的安装 # 在编写应用或运行示例之前，您可以使用Fyne安装工具检查您的安装。只需从链接下载适合您计算机的应用并运行它，您应该看到类似以下屏幕的内容：\n如果您的安装有任何问题，请查看故障排除 部分以获取提示。\n运行演示 # 如果您在开始编写自己的应用程序之前想看看Fyne工具包的实际运行情况，您可以通过执行以下命令在您的计算机上运行我们的演示应用：\n$ go run fyne.io/fyne/v2/cmd/fyne_demo@latest 请注意，首次运行需要编译一些C代码，因此可能比平时需要更长的时间。后续构建会重用缓存，将会快得多。\n旧版 Go 版本 # 要在旧版Go上运行演示，只需执行以下命令：\n$ go run fyne.io/fyne/v2/cmd/fyne_demo 安装 # 如果您愿意，您也可以使用以下命令安装演示（需要Go 1.16或更高版本）：\n$ go install fyne.io/fyne/v2/cmd/fyne_demo@latest 对于早期版本的Go，您需要使用以下命令：\n$ go get fyne.io/fyne/v2/cmd/fyne_demo 如果您的GOBIN环境变量已添加到路径中（在macOS和Windows上默认应该如此），那么您就可以运行演示了：\n$ fyne_demo 就这些了！\n现在你可以在你选择的IDE中编写自己的Fyne应用程序了。如果你想看到一些Fyne代码的实际运用，那么你可以阅读你的第一个应用程序。\n","date":"2024-10-16","externalUrl":null,"permalink":"/docs/gofyne/01-started/01-index/","section":"教程","summary":"使用 Fyne 工具包构建跨平台应用程序非常简单，但在开始之前需要安装一些工具。如果您的计算机已经为Go开发设置好了，以下步骤可能不是必需的，但我们建议您阅读一下您操作系统的提示，以防万一。如果本教程后面的步骤出现问题，那么您应该重新检查以下的先决条件。\n","title":"安装指南","type":"docs"},{"content":"","date":"2024-10-13","externalUrl":null,"permalink":"/docs/gofyne/01-started/","section":"教程","summary":"","title":"快速开始","type":"docs"},{"content":"每次打开终端，你看到的第一样东西是什么？默认情况下可能是 \u0026ldquo;Last login: Tue Mar 12 \u0026hellip;\u0026rdquo; 加上一行空的提示符。其实这个空间完全可以利用起来——显示系统状态、项目信息、或者纯粹让你心情好的东西。\nmotd 与欢迎脚本 # macOS/Linux 系统的 /etc/motd（Message of the Day）文件可以设置登录后显示的欢迎信息。\n系统级 motd # # 编辑系统级欢迎消息 sudo vim /etc/motd # 示例 cat /etc/motd 输出：\n╔══════════════════════════════╗ ║ Welcome to the Terminal ║ ║ Keep calm and code on ║ ╚══════════════════════════════╝ 用户级欢迎消息（推荐） # 我更推荐在 ~/.zshrc 中定义自己的欢迎消息，这样更灵活：\n# ~/.zshrc 末尾 echo \u0026#34;🚀 Hello, $(whoami)! Today is $(date \u0026#39;+%A, %Y-%m-%d\u0026#39;)\u0026#34; 显示待办与提醒 # 结合 todo.txt 或提醒工具，在终端启动时显示当天待办：\n# ~/.zshrc echo \u0026#34;\u0026#34; echo \u0026#34;📋 今日待办：\u0026#34; if [ -f ~/todo.txt ]; then grep \u0026#34;$(date \u0026#39;+%Y-%m-%d\u0026#39;)\u0026#34; ~/todo.txt || echo \u0026#34; ✅ 今天没有待办\u0026#34; fi echo \u0026#34;\u0026#34; 显示系统负载 # 更实用的做法是显示系统的关键状态：\nfunction show_sys_status() { local load=$(uptime | awk -F\u0026#39;load averages:\u0026#39; \u0026#39;{print $2}\u0026#39;) local disk=$(df -h / | awk \u0026#39;NR==2 {print $4, \u0026#34;free on\u0026#34;, $NF}\u0026#39;) local mem=$(vm_stat | awk \u0026#39;/free/ {print $3}\u0026#39; | tr -d \u0026#39;.\u0026#39;) local mem_mb=$((mem * 4096 / 1024 / 1024)) echo \u0026#34;⚡ 系统状态\u0026#34; echo \u0026#34; CPU负载:${load}\u0026#34; echo \u0026#34; 磁盘剩余: ${disk}\u0026#34; echo \u0026#34; 空闲内存: ${mem_mb}MB\u0026#34; } 一个完整的欢迎脚本示例 # # ~/.zshrc 或在 ~/.oh-my-zsh/custom/ 下创建一个新文件 function welcome_message() { clear echo \u0026#34;╔══════════════════════════════════════╗\u0026#34; echo \u0026#34;║ $(date \u0026#39;+%Y-%m-%d %A\u0026#39;)\u0026#34; echo \u0026#34;║ $(whoami)@$(hostname -s)\u0026#34; echo \u0026#34;╠══════════════════════════════════════╣\u0026#34; echo \u0026#34;║ 📂 $(pwd)\u0026#34; echo \u0026#34;║ ⏱ Uptime: $(uptime | awk -F\u0026#39;up\u0026#39; \u0026#39;{print $2}\u0026#39; | awk -F\u0026#39;,\u0026#39; \u0026#39;{print $1}\u0026#39;)\u0026#34; echo \u0026#34;╚══════════════════════════════════════╝\u0026#34; echo \u0026#34;\u0026#34; } # 调用 welcome_message 如果你想每次打开终端都显示，直接调用 welcome_message；如果只想在需要时使用，把它定义成函数，需要时手动执行。\n小结 # 终端启动消息是一个被很多人忽略的细节。花 5 分钟配置一个简单的欢迎面板，每次打开终端都能获得一点好心情——以及实用的系统状态信息。别让终端的第一眼永远是冰冷的 \u0026ldquo;Last login:\u0026quot;。\n","date":"2024-04-07","externalUrl":null,"permalink":"/docs/mytermenv/startup/welcome/","section":"教程","summary":"每次打开终端，你看到的第一样东西是什么？默认情况下可能是 “Last login: Tue Mar 12 …” 加上一行空的提示符。其实这个空间完全可以利用起来——显示系统状态、项目信息、或者纯粹让你心情好的东西。\n","title":"欢迎消息配置","type":"docs"},{"content":"我将先介绍平时工作中最常用的与目录文件相关的命令，分别是替换 ls 的 eza，替换 cd 的 zoxide 和替换 cat 的 bat。它们的优势会在文章中逐步展开说明。\n特别提醒：exa 已停止维护，以下内容以 eza（exa 的 fork）版本为准。\neza # eza 是一款可用于替换系统默认 ls 的命令。在平时工作中 ls 几乎是用得最多的命令，而 eza 在支持 ls 的基本能力基础上，提供了更丰富的特性。\n安装 # # macOS brew install eza # Debian/Ubuntu（需要手动添加仓库） sudo apt install eza # 或者直接从 GitHub 下载二进制 基础使用 # eza 默认行为已经比 ls 友好很多：\neza # 彩色输出，自动区分文件和目录 eza -l # 长格式，类似 ls -l eza -la # 包含隐藏文件 eza -T # 树形显示目录结构 eza -l --git # 显示 git 状态（哪些文件被修改） 彩色输出 # 默认 eza 就有颜色区分——目录蓝色、可执行文件绿色、图片文件紫色、符号链接粉色。而 ls 需要 --color=auto 才有基础的着色。\n树形视图 # 这是 eza 最亮眼的功能，一条命令就能看清项目结构：\n# 查看项目结构 eza -T --git-ignore # 树形，忽略 gitignore 中的文件 # 限定层级 eza -T -L 2 # 只展开两层 文件信息 # eza -lbg # 长格式 + 权限 + 用户 + 分组 eza -lbg --sort=size # 按文件大小排序 eza -lbg --sort=mod # 按修改时间排序 eza -lbg --icons # 显示文件类型图标（配合 Nerd Font） 加上 --icons 后，每个文件前都有对应的图标——目录 📁、图片 🖼️、代码 🧑‍💻，一目了然。\n配置别名 # 建议在 ~/.zshrc 中添加别名，替换掉默认 ls：\nalias ls=\u0026#34;eza\u0026#34; alias ll=\u0026#34;eza -lbg\u0026#34; alias la=\u0026#34;eza -labg\u0026#34; alias lt=\u0026#34;eza -T -L 2\u0026#34; zoxide # zoxide 是基于 frecency（frequency + recency）算法的智能 cd 命令。它会记录你访问过的目录，让你在\u0026quot;猜你想去哪个目录\u0026quot;这件事上做到几乎零思考。用上之后，你会发现自己越来越少打完整的 cd 命令了。\n安装 # # macOS brew install zoxide # 其他系统 curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash 配置 # 将 zoxide 接入 zsh，只需在 ~/.zshrc 中加一行：\neval \u0026#34;$(zoxide init zsh)\u0026#34; 这会注册 z 命令作为 zoxide 的入口。\n使用方式 # 安装前先多 cd 到各个目录，让 zoxide 积累数据。之后只要一个关键字就能跳转：\n# 先到各个目录走动，让 zoxide 学习 cd ~/Projects/myapp/src/components cd ~/Documents/work/reports cd ~/Downloads # 之后想去哪里，一个关键字就够了 z comp # → ~/Projects/myapp/src/components z reports # → ~/Documents/work/reports z down # → ~/Downloads 多关键字匹配 # z myapp src # 多个关键字缩小范围 z work report # → ~/Documents/work/reports 核心逻辑 # zoxide 的魔力在于它不只是匹配路径名，而是综合考虑了访问频率和最近访问时间：\n常去的目录 → 权重大 最近刚去过 → 加分 很久以前的 → 权重衰减 如果你有多个同名目录，常用那个自动排第一。\n交互模式 # 如果多个目录匹配，用交互模式选择：\nzi proj # 弹出 fzf 交互选择器 cd 兼容 # zoxide 完全兼容原生 cd——输入完整路径时直接 cd，输入关键字时智能跳转：\nz /etc/nginx # 完整路径，行为同 cd z nginx # 关键字，跳转到匹配的目录 bat # bat 是一款支持语法高亮、Git 集成的用于替换 cat 的文件查看命令。它和百度/阿里/腾讯没有关系。\n安装 # # macOS brew install bat # Debian/Ubuntu sudo apt install bat 基础使用 # bat main.py # 语法高亮 + 行号 bat --line-range 10:20 main.py # 只显示 10-20 行 语法高亮 # 默认输出带行号、语法高亮和 Git 修改标记：\n1 │ package main 2 │ 3 │ import \u0026#34;fmt\u0026#34; 4 │ 5 │ func main() { 6 │ fmt.Println(\u0026#34;Hello\u0026#34;) 7 │ } ← 行号左侧如果有 + 或 - 表示 git 修改 bat 自动检测 100+ 种语言，几乎所有编程语言和配置文件都覆盖了。\n与管道兼容 # 当 bat 的输出被 pipe 到其他命令时，自动禁用高亮和分页，行为同 cat：\nbat main.py | head -20 # 自动降级 bat main.py \u0026gt; output.txt # 也自动降级 bat --plain main.py # 强制原始输出 整合到其他工具 # # 结合 ripgrep rg pattern --context 3 myfile.py | bat # 结合 diff diff file1.py file2.py | bat -l diff 配置 # 通过 ~/.config/bat/config 或 BAT_CONFIG_PATH 环境变量指定配置文件：\n--theme=\u0026#34;Dracula\u0026#34; --number --paging=never ","date":"2024-03-24","externalUrl":null,"permalink":"/docs/mytermenv/commands/filesystem/","section":"教程","summary":"我将先介绍平时工作中最常用的与目录文件相关的命令，分别是替换 ls 的 eza，替换 cd 的 zoxide 和替换 cat 的 bat。它们的优势会在文章中逐步展开说明。\n特别提醒：exa 已停止维护，以下内容以 eza（exa 的 fork）版本为准。\n","title":"文件目录","type":"docs"},{"content":"本节介绍 oh-my-zsh 的安装与主题。\n安装 # oh-my-zsh 是 zsh 最流行的配置管理框架，安装极其简单：\n# curl 安装 sh -c \u0026#34;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; # 或者用 wget sh -c \u0026#34;$(wget -O- https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; 安装脚本会自动备份你当前的 ~/.zshrc 为 ~/.zshrc.pre-oh-my-zsh，然后创建一份新的 ~/.zshrc。\n目录结构 # 安装完成后，oh-my-zsh 的目录结构如下：\n~/.oh-my-zsh/ ├── custom/ # 自定义配置和插件 ├── plugins/ # 内置插件（300+） ├── themes/ # 内置主题 ├── lib/ # 核心函数库 ├── templates/ # 模板文件 └── oh-my-zsh.sh # 入口文件 切换主题 # oh-my-zsh 内置了大量主题。修改 ~/.zshrc 中的 ZSH_THEME 字段即可切换：\n# ~/.zshrc ZSH_THEME=\u0026#34;robbyrussell\u0026#34; # 默认主题 ZSH_THEME=\u0026#34;agnoster\u0026#34; # 经典主题，显示 git 信息 ZSH_THEME=\u0026#34;ys\u0026#34; # 简洁实用 ZSH_THEME=\u0026#34;avit\u0026#34; # 信息丰富的双行提示 ZSH_THEME=\u0026#34;random\u0026#34; # 每次随机换一个主题 设为 random 后，每次打开终端都是惊喜。如果遇到了特别喜欢的，zsh 会告诉你当前用的主题名。\n常用主题推荐 # agnoster — 最受欢迎的第三方风格主题，显示 git 分支、状态等 ys — 双行提示，上行列路径，下一行输入 candy — 简洁的彩色双行主题 dst — 左侧显示时间+路径，右侧显示 git 信息 bira — 美观的箭头提示符 不过，更多高级用户会选择不依赖 oh-my-zsh 内置主题，而是安装 Powerlevel10k——它的自定义能力远超内置主题，下一篇会专门介绍。\n小结 # oh-my-zsh 的安装和主题切换就是这么简单。接下来我们看看它最强大的部分——插件系统。\n","date":"2024-02-18","externalUrl":null,"permalink":"/docs/mytermenv/ohmyzsh/install/","section":"教程","summary":"本节介绍 oh-my-zsh 的安装与主题。\n安装 # oh-my-zsh 是 zsh 最流行的配置管理框架，安装极其简单：\n# curl 安装 sh -c \"$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\" # 或者用 wget sh -c \"$(wget -O- https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\" 安装脚本会自动备份你当前的 ~/.zshrc 为 ~/.zshrc.pre-oh-my-zsh，然后创建一份新的 ~/.zshrc。\n","title":"安装与主题","type":"docs"},{"content":" 安装 # 对于不同系统，zsh 的安装命令，如下所示：\nmacOS：\nmacOS 从 Catalina 开始默认 shell 就是 zsh 了，无需额外安装。如果你的系统版本较旧：\nbrew install zsh Debian/Ubuntu：\nsudo apt install zsh CentOS/RHEL/Fedora：\nsudo dnf install zsh # Fedora sudo yum install zsh # CentOS 7 设为默认 Shell # 安装完成后，将其设为默认登录 shell：\nchsh -s $(which zsh) 重新打开终端，你就进入了 zsh 的世界。第一次启动 zsh 时，会看到初始配置向导——按 q 跳过，我们后面通过 oh-my-zsh 统一管理配置。\n验证是否切换成功：\necho $SHELL # 输出应该是 /bin/zsh 或 /usr/bin/zsh echo $ZSH_VERSION # 输出版本号，如 5.9 基础配置 # zsh 的配置文件是 ~/.zshrc，所有的别名、函数、插件配置都在这个文件里。如果你是从 bash 迁移过来的，可以把 ~/.bashrc 里的配置搬过来，大部分语法兼容。\n不过别急着手动配置——下一章介绍的 oh-my-zsh 会让配置管理变得极其简单。\n","date":"2024-02-04","externalUrl":null,"permalink":"/docs/mytermenv/zsh/install/","section":"教程","summary":"安装 # 对于不同系统，zsh 的安装命令，如下所示：\nmacOS：\nmacOS 从 Catalina 开始默认 shell 就是 zsh 了，无需额外安装。如果你的系统版本较旧：\n","title":"快速安装","type":"docs"},{"content":"iTerm2 是一款终端软件，它是 macOS 下默认终端 Terminal 的替代品。每次拿到新电脑，或者因某种原因重装系统，我首先要做的就是下载 iTerm2 来替换默认的终端 terminal。\niTerm2 vs 默认 Terminal # 为什么要用 iTerm2 而不是 Mac 自带的 Terminal？一句话总结：默认 Terminal 能用，但 iTerm2 更好用。\n特性 默认 Terminal iTerm2 分屏（Split Panes） ❌ ✅ 热键窗口（Hotkey Window） ❌ ✅ 搜索高亮 基础 正则+高亮 配置文件（Profiles） ❌ ✅ 粘贴历史 ❌ ✅ 即时重放（Instant Replay） ❌ ✅ 256 色 / True Color ❌ ✅ 自定义快捷键 有限 丰富 核心亮点 # 分屏 # Command + D 垂直分屏，Command + Shift + D 水平分屏，告别开多个窗口来回切换的烦恼。\n热键窗口 # 设置一个全局快捷键（如 Option + Space），不管你在哪个应用，一键呼出/隐藏终端，用完即走。对频繁使用终端的人来说，这个特性一旦习惯就回不去了。\n搜索与即跳 # Command + F 搜索，支持正则。而且 iTerm2 的搜索结果可以自动跳转到对应行，无需在满屏输出里肉眼找。\n配置文件 # 你可以为不同场景创建不同的 Profile，比如：\n默认：日常开发，白底或暗色主题 SSH：连接服务器时自动执行 ssh 命令 透明：需要边看文档边敲命令时用半透明背景 每个 Profile 可以独立设置字体、颜色、背景图、快捷键，启动时自动执行命令。\n粘贴历史 # Command + Shift + H 打开粘贴历史，所有你复制过的内容都在这里。再也不用因为 Ctrl+C 覆盖了剪贴板而懊恼。\n小结 # iTerm2 不是那种\u0026quot;装了也没什么感觉\u0026quot;的工具，而是一个一旦用上就回不去的终端增强工具。后续文章会一步步介绍它的安装、配置和使用技巧。\n","date":"2024-01-07","externalUrl":null,"permalink":"/docs/mytermenv/terminal/hello/","section":"教程","summary":"iTerm2 是一款终端软件，它是 macOS 下默认终端 Terminal 的替代品。每次拿到新电脑，或者因某种原因重装系统，我首先要做的就是下载 iTerm2 来替换默认的终端 terminal。\niTerm2 vs 默认 Terminal # 为什么要用 iTerm2 而不是 Mac 自带的 Terminal？一句话总结：默认 Terminal 能用，但 iTerm2 更好用。\n","title":"简要介绍","type":"docs"},{"content":"终端是程序员日常必不可少的工具之一，特别是如果你的系统是 MacOS 或 Linux 的话，终端的地位更是遥遥领先。\n本文是搭建我的终端环境系列中的第一篇，首先将介绍第一个必不可可少的工具终端 - iTerm2，Mac 上的终端神器。\n故而，我们的开篇第一节先介绍 iTerm2 的使用方法。我将主要介绍如何安装与配置 iTerm2，安装成功后，会带着一起体验的一些能力。\n","externalUrl":null,"permalink":"/docs/mytermenv/terminal/","section":"教程","summary":"终端是程序员日常必不可少的工具之一，特别是如果你的系统是 MacOS 或 Linux 的话，终端的地位更是遥遥领先。\n本文是搭建我的终端环境系列中的第一篇，首先将介绍第一个必不可可少的工具终端 - iTerm2，Mac 上的终端神器。\n","title":"iTerm2","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/01-intro-install/","section":"教程","summary":"","title":"介绍与安装","type":"docs"},{"content":" 在写复杂策略前，先搭好运行环境很重要。Backtrader 的核心是 Cerebro，它负责管理数据、策略和资金。\n最小化示例 # 如下是最简单的初始配置代码：\nimport backtrader as bt if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() print(f\u0026#39;初始资金: {cerebro.broker.getvalue()}\u0026#39;) cerebro.run() print(f\u0026#39;最终资金: {cerebro.broker.getvalue()}\u0026#39;) 代码说明 # 先导入 backtrader 模块并命名为 bt，然后基于 bt.Cerebro 实例化引擎。\nimport backtrader as bt cerebro = bt.Cerebro() 通过 cerebro.broker.getvalue() 获取并打印初始组合价值。接着运行 cerebro.run() 迭代数据进行模拟交易，完成后再次打印最终的持仓组合价值。\ncerebro.run() print(f\u0026#39;最终资金: {cerebro.broker.getvalue()}\u0026#39;) 输出结果 # 初始资金: 10000.00 最终资金: 10000.00 小结 # 这个简单示例中，Cerebro 引擎在后台自动创建了 broker 实例并分配了初始资金。如未明确设置 broker，系统会使用默认配置，初始资金为 10,000 货币单位。\n接下来的章节会对 broker 进行更多配置，逐步添加数据源（DataFeed）、策略（Strategy）、指标（Indicator）等，完善这个交易系统。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/01-setup/","section":"教程","summary":" 在写复杂策略前，先搭好运行环境很重要。Backtrader 的核心是 Cerebro，它负责管理数据、策略和资金。\n","title":"Cerebro 初始配置","type":"docs"},{"content":"Cerebro 是 Backtrader 的核心类，负责整个系统的运行。\n它的功能包括：\n管理数据源、策略、观察者、分析器和编写器，确保系统正常运行。 执行回测或实时数据供给和交易。 返回回测结果。 提供策略绘图功能。 创建 Cerebro 实例 # 创建 Cerebro 实例时，可以通过传递一些控制参数：\ncerebro = bt.Cerebro(**kwargs) 这些参数会影响系统的执行，具体说明可参考文档（这些参数也可传递给 run 方法使用）。\n添加数据源 # 最常见的方式是使用 cerebro.adddata(data) 添加数据源，data 是已实例化的数据源。例如：\ndata = bt.BacktraderCSVData(dataname=\u0026#39;mypath.days\u0026#39;, timeframe=bt.TimeFrame.Days) cerebro.adddata(data) 数据的重采样与重放 # Cerebro 也支持对数据进行重采样或重放：\n重采样：\ndata = bt.BacktraderCSVData(dataname=\u0026#39;mypath.min\u0026#39;, timeframe=bt.TimeFrame.Minutes) cerebro.resampledata(data, timeframe=bt.TimeFrame.Days) 重放数据：\ndata = bt.BacktraderCSVData(dataname=\u0026#39;mypath.min\u0026#39;, timeframe=bt.TimeFrame.Minutes) cerebro.replaydata(data, timeframe=bt.TimeFrame.Days) 可同时使用多种类型的数据源，包括常规数据、重采样数据和重放数据。但需确保时间对齐。详见文档中的 多时间框架 和 数据重采样 部分。\n添加策略 # Cerebro 接收策略类及相关参数，即使不进行优化，也可通过以下方式添加：\ncerebro.addstrategy(MyStrategy, myparam1=value1, myparam2=value2) 策略优化 # 在优化时，参数需要作为可迭代对象传递。例如：\ncerebro.optstrategy(MyStrategy, myparam1=range(10, 20)) 这将运行 MyStrategy 10 次，myparam1 取值从 10 到 19。\n添加其他组件 # 可通过以下方法为回测添加额外功能：\naddwriter：记录回测数据。 addanalyzer：分析回测结果。 addobserver 或 addobservermulti：添加观察者，实时跟踪策略执行。 自定义经纪人 # Cerebro 默认使用 Backtrader 内建的经纪人，但也可自定义：\nbroker = MyBroker() cerebro.broker = broker # 通过 getbroker/setbroker 方法设置 接收通知 # 数据源或经纪人可能会发出通知。Cerebro 通过 notify_store 方法接收这些通知。处理通知有三种方式：\n使用回调函数 # 可通过 addnotifycallback(callback) 添加回调函数，签名如下：\ncallback(msg, *args, **kwargs) 在策略中覆盖 notify_store 方法 # 也可在策略类中覆盖 notify_store 方法处理通知，签名如下：\ndef notify_store(self, msg, *args, **kwargs): # 处理通知 子类化 Cerebro # 子类化 Cerebro 并覆盖 notify_store 方法，但这是最不推荐的方式。\n执行回测 # 通过以下方法执行回测：\nresult = cerebro.run(**kwargs) run 方法支持多种参数，可在实例化或运行时指定，详细说明可参考文档。\n标准观察者 # Cerebro 默认实例化三个标准观察者：\n经纪人观察者：追踪现金和投资组合的价值。 交易观察者：记录每笔交易的效果。 买卖观察者：记录操作的执行时间。 如果不需要这些标准观察者，可以通过 stdstats=False 禁用它们。\n返回回测结果 # 回测执行后，Cerebro 返回策略实例，供进一步分析。可访问策略中的所有元素进行详细检查：\nresult = cerebro.run(**kwargs) 优化时的返回结果 # 未优化时，result 是一个策略实例的列表。 启用优化时，result 是一个列表的列表，每个子列表对应一次优化运行后的策略实例。 注意：优化时默认只返回分析器结果，完整的策略结果需通过设置 optreturn=False 获取。\n提供绘图功能 # 如果安装了 matplotlib，可通过以下方式绘制回测图形：\ncerebro.plot() 回测流程概述 # 回测的执行逻辑大致如下：\n传递任何存储的通知。 数据源提供下一组 tick/条。 数据同步：多个数据源的数据按时间对齐，确保不同时间框架的数据可同时计算。 版本更新：1.9.0.99 版本引入了新的同步行为。 通知策略有关订单、交易和现金的更新。 经纪人接受排队订单，并根据新数据执行订单。 调用策略的 next 方法，评估新数据并执行策略逻辑。 更新观察者、指标、分析器等，触发其他活动。 通过编写器将数据写入目标。 重要注意事项 # 在数据源传递新的一组数据时，这些数据是“已关闭”的。这意味着策略发出的订单将基于 下一条数据 执行，而不是当前数据。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/05-cerebro/01-cerebro/","section":"教程","summary":"Cerebro 是 Backtrader 的核心类，负责整个系统的运行。\n它的功能包括：\n管理数据源、策略、观察者、分析器和编写器，确保系统正常运行。 执行回测或实时数据供给和交易。 返回回测结果。 提供策略绘图功能。 创建 Cerebro 实例 # 创建 Cerebro 实例时，可以通过传递一些控制参数：\n","title":"Cerebro 回测引擎详解","type":"docs"},{"content":"本节介绍 backtrader 中数据源的配置与使用，以及一些数据访问的技巧。\n数据配置 # 在 Backtrader 中，数据源 DataFeed 通过 Cerebro 配置。\n配置代码：\ncerebro = bt.Cerebro() data = btfeeds.MyFeed(...) cerebro.adddata(data) cerebro.addstrategy(MyStrategy, period=30) 通过 cerebro.adddata 将 DataFeed 添加到系统中。我们无需关心系统是如何接收 DataFeed 的。\n使用方法 # 策略中，通过 self.datas 数组即可访问数据。看一个简单示例：\n示例如下：\nclass MyStrategy(bt.Strategy): params = dict(period=20) def __init__(self): sma = btind.SimpleMovingAverage(self.datas[0], period=self.params.period) 通过 self.datas[0] 即可访问数据。\n示例中有两个注意点：\n策略的 __init__ 方法无需接收 *args 或 **kwargs； self.datas 是一个包含 DataFeed 的数组，至少包含一个数据源，否则会出现异常； 数据源添加到系统后，在策略实现中可按添加顺序访问每个数据源。\ncerebro.adddata(data0) cerebro.adddata(data1) 在策略类访问：\nself.datas[0] # data0 self.datas[1] # data1 快捷访问 # 数据源也可通过快捷方式访问，self.datas 数组中的每个元素都有自动生成的成员变量：\n对应规则：\nself.data 对应的是 self.datas[0] self.dataX 对应的是 self.datas[X] 示例：\nclass MyStrategy(bt.Strategy): params = dict(period=20) def __init__(self): sma = btind.SimpleMovingAverage(self.data, period=self.params.period) 示例中的 self.data 是 self.datas[0] 的快捷方式，即访问的就是第一个数据源。如果你添加了多个数据源，就可以通过 self.data1 访问 self.datas[1]，self.data2 访问 self.datas[2]，依次类推。\n省略数据源 # 上面的示例还可以进一步简化。\n调用 SimpleMovingAverage 时，可以完全省略 self.data，backtrader 会自动选择数据源：\nclass MyStrategy(bt.Strategy): params = dict(period=20) def __init__(self): sma = btind.SimpleMovingAverage(period=self.params.period) 在这个简化版本中，SimpleMovingAverage 没有显式传递 self.data。\n省略 self.data 后，SimpleMovingAverage 会默认使用第一个数据源（即 self.data 或 self.datas[0]）作为输入。\n数据延伸 # 在 Backtrader 中，不仅数据源可以作为输入，指标和计算结果同样可以作为数据提供给策略。\n通过一个示例来说明。\n将数据源作为输入计算指标：\nsma1 = btind.SimpleMovingAverage(self.datas[0], period=self.p.period1) self.datas[0] 是第一个数据源，传递给 SimpleMovingAverage 进行计算。\n基于计算出的指标生成新的变量，如：\nsma2 = btind.SimpleMovingAverage(sma1, period=self.p.period2) 还可以在指标之间进行运算，将操作结果作为新的变量：\ndiff = sma2 - sma1 + self.data.close 操作结果可以像数据源一样传递给下一个指标计算函数：\nsma3 = btind.SimpleMovingAverage(diff, period=self.p.period3) greater = sma3 \u0026gt; sma1 sma4 = btind.SimpleMovingAverage(greater, period=self.p.period4) 上述步骤中，不断将中间计算结果作为新数据源，传递给后续指标进一步计算。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/01-datafeed/","section":"教程","summary":"本节介绍 backtrader 中数据源的配置与使用，以及一些数据访问的技巧。\n数据配置 # 在 Backtrader 中，数据源 DataFeed 通过 Cerebro 配置。\n","title":"DataFeed 数据源详解","type":"docs"},{"content":"与盈透（Interactive Brokers）的集成支持以下功能：\n实时数据馈送 实时交易 注意：虽然已尽力测试了各种错误条件，但代码（像任何软件一样）仍可能包含错误。上线前，请使用模拟交易账户或 TWS 演示账户充分测试策略。\n注意：与盈透的交互通过 IbPy 模块进行，使用前必须先安装。目前 PyPI 中暂没有该模块的包，但可以通过 pip 安装：\npip install git+https://github.com/blampe/IbPy.git 如果你的系统中没有 git（例如在 Windows 上），也可以直接运行：\npip install https://github.com/blampe/IbPy/archive/master.zip 示例代码 # 源码包含一个完整的示例，位于：\nsamples/ibtest/ibtest.py\n该示例无法覆盖所有场景，但力求提供广泛的参考，并说明回测模块与实时数据模块在使用上没有本质区别。\n注意：\n示例在交易活动开始前会等待 data.LIVE 状态通知。实现实时策略时通常需要考虑这一点。\n存储模型与直接模型 # 与互动经纪商的交互支持两种模型：\n存储模型（推荐） 直接与数据馈送类和经纪商类交互 存储模型清晰分离了经纪商和数据的创建方式。以下两个代码片段展示了具体用法。\n首先是存储模型：\nimport backtrader as bt ibstore = bt.stores.IBStore(host=\u0026#39;127.0.0.1\u0026#39;, port=7496, clientId=35) data = ibstore.getdata(dataname=\u0026#39;EUR.USD-CASH-IDEALPRO\u0026#39;) 参数说明：\nhost、port 和 clientId 传入 IBStore 以打开连接。 getdata 创建数据馈送，并通过参数 dataname 请求 EUR/USD 外汇对。 直接使用模型：\nimport backtrader as bt data = bt.feeds.IBData(dataname=\u0026#39;EUR.USD-CASH-IDEALPRO\u0026#39;, host=\u0026#39;127.0.0.1\u0026#39;, port=7496, clientId=35) 在这里：\n参数直接传递给数据，会在后台自动创建 IBStore 实例。 缺点是：\n不够清晰，因为无法区分哪些参数属于数据、哪些属于存储。 IBStore - 存储 # 存储是实时数据馈送/交易支持的核心，提供了 IbPy 与数据馈送、经纪代理之间的适配层。\n存储的概念涵盖以下功能：\n作为中央实体：此处实体为 IB，可以接受或不接受参数。 提供获取经纪实例的方法： IBStore.getbroker(*args, **kwargs) 提供获取数据馈送实例的方法： IBStore.getdata(*args, **kwargs) 许多 **kwargs 是数据馈送的通用参数，如 dataname、fromdate、todate、sessionstart、sessionend、timeframe、compression。\n数据可能会提供其他参数。请检查下面的参考。\nIBStore 提供：\n连接目标（host 和 port 参数） 身份识别（clientId 参数） 重新连接控制（reconnect 和 timeout 参数） 时间偏移检查（timeoffset 参数，见下文） 通知与调试 notifyall (default: False): IB 发送的任何错误消息（许多仅为信息性）将转发到 Cerebro/Strategy。 _debug (default: False): TWS 接收到的每条消息均打印到标准输出。 IBData 数据馈送 # 数据选项 # 无论是直接还是通过 getdata，IBData 数据馈送都支持以下数据选项：\n历史下载请求\n如果持续时间超过 IB 为给定时间框架/压缩组合设置的限制，这些请求将被拆分为多个请求。\n实时数据有三种模式\ntickPrice 事件（通过 IB reqMktData）\n用于 CASH 产品（根据至少 TWS API 9.70 的实验，其他类型不支持）\n通过查看 BID 价格接收 tick 价格事件，根据非官方互联网文献，这似乎是跟踪 CASH 市场价格的方式。\n时间戳在系统中本地生成。如果最终用户希望，可以使用与 IB 服务器时间的偏移（从 IB reqCurrentTime 计算）。\ntickString 事件（即 RTVolume（通过 IB reqMktData）\n从 IB 大约每 250 毫秒接收一次 OHLC/成交量快照（如果没有交易发生，则更长时间）。\nRealTimeBars 事件（通过 IB reqRealTimeBars）\n每 5 秒接收一次历史 5 秒的条形图（由 IB 固定的持续时间）。\n如果选择的时间框架/组合低于 Seconds/5 级别，此功能将自动禁用。\n注意：RealTimeBars 不适用于 TWS 演示。\n默认行为是在大多数情况下使用：tickString，除非用户特别希望使用 RealTimeBars。\n回填\n除非用户只请求历史下载，否则数据馈送会自动回填：\n启动时：回填最大可能的数据量。例如，对于 Days/1 组合，IB 默认最大持续时间为 1 年，这就是回填的量。\n断开连接后：根据断开前收到的最近数据，尽量下载最少的数据来完成回填。\n注意：最终的时间框架/压缩组合可能不是在创建数据馈送时指定的，而是在插入系统时指定的。请看以下示例：\ndata = ibstore.getdata(dataname=\u0026#39;EUR.USD-CASH-IDEALPRO\u0026#39;, timeframe=bt.TimeFrame.Seconds, compression=5) cerebro.resampledata(data, timeframe=bt.TimeFrame.Minutes, compression=2) 现在应该清楚，最终考虑的时间框架/压缩组合是 Minutes/2。\n数据合约检查 # 在启动阶段，数据馈送会尝试下载指定合约的详细信息（参阅参考了解如何指定）。如果未找到合约或找到多个匹配项，数据将拒绝继续并通知系统。示例：\ndata = ibstore.getdata(dataname=\u0026#39;TWTR\u0026#39;) # 推特 由于默认类型为 STK，交易所为 SMART，货币为默认（空），将找到一个以 USD 交易的合约（2016-06）。\n同样方式对 AAPL 会失败：\ndata = ibstore.getdata(dataname=\u0026#39;AAPL\u0026#39;) # 错误 -\u0026gt; 多个合约 因为 SMART 找到在多个实际交易所中的合约，且 AAPL 在其中一些以不同货币交易。以下是可以的：\ndata = ibstore.getdata(dataname=\u0026#39;AAPL-STK-SMART-USD\u0026#39;) # 找到1个合约 数据通知 # 数据馈送将通过以下方式报告当前状态（查阅 Cerebro 和 Strategy 参考）：\nCerebro.notify_data（如果覆盖） 使用 Cerebro.adddatacb 添加的回调 Strategy.notify_data（如果覆盖） 策略中的示例如下：\nclass IBStrategy(bt.Strategy): def notify_data(self, data, status, *args, **kwargs): if status == data.LIVE: # 数据已切换为实时数据 # 执行某些操作 pass 系统状态变化时，会发送以下通知：\nCONNECTED：成功初始连接后发送 DISCONNECTED：无法检索数据，系统无法继续。可能的原因： 指定的合约错误 历史下载中断 超过与 TWS 的重连次数 CONNBROKEN：与 TWS 或数据服务器的连接丢失。数据馈送会尝试重新连接，必要时回填并恢复操作。 NOTSUBSCRIBED：合约和连接正常，但因权限不足无法检索数据。系统无法继续。 DELAYED：历史/回填操作进行中，策略处理的数据不是实时数据。 LIVE：策略从此刻开始处理的是实时数据。 策略开发者应考虑处理断连和延迟数据的策略。\n数据时间框架和压缩 # backtrader 生态中的数据馈送在创建时支持 timeframe 和 compression 参数。这些参数也可作为属性访问，如 data._timeframe 和 data._compression。\n时间框架/压缩组合通过 resampledata 或 replaydata 传递给 cerebro，告知内部重采样器/重放器目标值。在重采样/重放过程中，_timeframe 和 _compression 会被覆盖。\n但在实时数据馈送中，这些信息可能起关键作用。请看以下示例：\ndata = ibstore.getdata(dataname=\u0026#39;EUR.USD-CASH-IDEALPRO\u0026#39;, timeframe=bt.TimeFrame.Ticks, compression=1, # 1 是默认值 rtbar=True, # 使用实时条 ) cerebro.adddata(data) 用户请求 tick 数据，这很重要，因为：\n不会进行回填（IB 支持的最小单位是 Seconds/1） 即使请求和支持实时条，如果时间分辨率低于 Seconds/5，也不会使用它们。 除非以 Ticks/1 分辨率工作，否则数据必须重采样/重放。以下是使用实时条的例子：\ndata = ibstore.getdata(dataname=\u0026#39;TWTR-STK-SMART\u0026#39;, rtbar=True) cerebro.resampledata(data, timeframe=bt.TimeFrame.Seconds, compression=20) 如上所述，data._timeframe 和 data._compression 属性将在 resampledata 期间被覆盖。以下是将发生的情况：\n回填将请求 Seconds/20 分辨率 实时条将用于实时数据，因为分辨率等于或大于 Seconds/5，数据支持（不是 CASH 产品） 来自 TWS 的事件最多每 5 秒发生一次。这可能并不重要，因为系统每 20 秒只会向策略发送一个条。 没有实时条的情况：\ndata = ibstore.getdata(dataname=\u0026#39;TWTR-STK-SMART\u0026#39;) cerebro.resampledata(data, timeframe=bt.TimeFrame.Seconds, compression=20) 在这种情况下：\n回填将请求 Seconds/20 分辨率 tickString 将用于实时数据（不是 CASH 产品） 来自 TWS 的事件最多每 250 毫秒发生一次。这可能并不重要，因为系统每 20 秒只会向策略发送一个条。 最后是 CASH 产品，时间跨度为 20 秒：\ndata = ibstore.getdata(dataname=\u0026#39;EUR.USD-CASH-IDEALPRO\u0026#39;) cerebro.resampledata(data, timeframe=bt.TimeFrame.Seconds, compression=20) 在这种情况下：\n回填将请求 Seconds/20 分辨率 tickPrice 将用于实时数据，因为这是一个现金产品 即使添加了 rtbar=True 来自 TWS 的事件最多每 250 毫秒发生一次。这可能并不重要，因为系统每 20 秒只会向策略发送一个条。 时间管理 # 数据馈送将自动从 TWS 报告的 ContractDetails 对象中确定时区。\n注意：这需要安装 pytz。如果未安装，用户应提供 tz 参数，一个兼容 tzinfo 的实例，用于所需的输出时区。\n注意：如果安装了 pytz，且用户觉得自动时区确定不起作用，tz 参数可以包含一个时区名称字符串。backtrader 将尝试使用给定名称实例化一个 pytz.timezone。\n报告的日期时间将是与产品相关的时区。例如：\n产品：Eurex 的 EuroStoxxx 50（代码：ESTX50-YYYYMM-DTB） 时区将是 CET（中欧时间），即 Europe/Berlin 产品：ES-Mini（代码：ES-YYYYMM-GLOBEX） 时区将是 EST5EDT，即 EST，即 US/Eastern 产品：EUR.JPY 外汇对（代码：EUR.JPY-CASH-IDEALPRO） 时区将是 EST5EDT，即 EST，即 US/Eastern 实际上，这是一个互动经纪商的设置，因为外汇对几乎 24 小时不间断交易，因此不会有真实的时区。\n这种行为确保无论交易者的实际位置如何，交易都保持一致，因为计算机很可能具有交易地点的实际时区，而不是交易场所的时区。\n请阅读手册中的时间管理部分。\n注意：TWS 演示在报告没有数据下载权限的资产时区方面不准确（EuroStoxx 50 期货是这种情况的一个例子）。\n实时数据馈送和重采样/重放 # 实时数据馈送何时交付条？设计原则是：尽可能实时交付。对于 Ticks 时间框架来说确实如此，但如果涉及重采样/重放，可能会有延迟。例如：\n重采样配置为 Seconds/5：\ncerebro.resampledata(data, timeframe=bt.TimeFrame.Seconds, compression=5) 一个时间戳为 23:05:27.325000 的 tick 被传递。\n市场交易缓慢，下一个 tick 在 23:05:59.025000 才到来。\n问题在于，backtrader 无法预知下一个 tick 要等 32 秒。如果不做处理，时间戳为 23:05:30.000000 的重采样条将延迟约 29 秒才能交付。\n因此，实时数据馈送会定期唤醒（每 x 秒），通知重采样器/重放器没有新数据进来。这是由参数 qcheck（默认值：0.5 秒）控制的。\n这样，重采样器每 qcheck 秒就有一次机会交付条。前述场景中的重采样条（23:05:30.000000）最多在报告时间后的 qcheck 秒内即可交付。\n由于默认值是 0.5 秒，最迟交付时间约为 23:05:30.500000，比原来早了近 29 秒。\n缺点是：\n某些 tick 可能到达太晚，无法纳入已交付的重采样/重放条。例如，如果在条交付后，TWS 才收到一个时间戳为 23:05:29.995000 的延迟消息，这个消息对已报告的 23:05:30.000000 来说已经太迟。\n这主要发生在以下情况：\ntimeoffset 在 IBStore 中被禁用（设为 False）且 IB 报告时间与本地时钟有明显差异时。\n避免大多数延迟样本的最佳方法：\n增大 qcheck 值，给延迟消息留出空间：\ndata = ibstore.getdata(\u0026#39;TWTR\u0026#39;, qcheck=2.0, ...) 这应该增加额外的空间，即使它会延迟重采样/重放条的交付。\n注意：当然，对于 Seconds/5 重采样来说，2.0 秒的延迟与 Minutes/10 的重采样有不同的意义。\n如果希望禁用 timeoffset 并且不通过 qcheck 管理，仍然可以处理延迟样本：\n使用 _latethrough 设置为 True 作为 getdata / IBData 的参数：\ndata = ibstore.getdata(\u0026#39;TWTR\u0026#39;, _latethrough=True, ...) 在重采样/重放时使用 takelate 设置为 True：\ncerebro.resampledata(data, takelate=True) IBBroker - 实时交易 # 注意：应需求，模拟经纪商中实现了 tradeid 功能，用于正确分配不同 tradeid 的佣金。\n但实时经纪商无法做到这一点，因为佣金是在无法区分不同 tradeid 的时间点报告的。\n因此，虽然仍可以指定 tradeid，但已不再有意义。\n使用经纪商 # 要使用 IB 经纪商，必须替换 cerebro 创建的标准模拟经纪实例。\n使用存储模型（推荐）：\nimport backtrader as bt cerebro = bt.Cerebro() ibstore = bt.stores.IBStore(host=\u0026#39;127.0.0.1\u0026#39;, port=7496, clientId=35) cerebro.broker = ibstore.getbroker() # 或者 cerebro.setbroker(...) 直接使用方法：\nimport backtrader as bt cerebro = bt.Cerebro() cerebro.broker = bt.brokers.IBBroker(host=\u0026#39;127.0.0.1\u0026#39;, port=7496, clientId=35) 经纪商参数 # 无论是直接还是通过 getbroker，IBBroker 都不支持任何参数。因为经纪商只是代理，不应屏蔽真实经纪商提供的功能。\n一些限制 # 现金和价值报告 # 内部模拟经纪商在策略 next 方法之前计算净值和现金，而实时经纪商无法保证这一点。\n如果请求这些值，next 的执行可能会延迟到收到响应。\n经纪商可能尚未计算这些值。\nbacktrader 通知 TWS 提供更新值，但无法预知消息何时到达。\nIBBroker 的 getcash 和 getvalue 方法始终返回从 IB 收到的最新值。\n注意：另一个限制是，这些值以账户的基础货币报告，即使有其他货币的值。这是设计上的选择。\n头寸 # backtrader 使用 TWS 报告的资产头寸（价格和数量）。也可以通过订单执行和状态消息内部计算，但如果丢失了部分消息，计算结果将不准确。\n此外，如果连接 TWS 时资产已有未平仓头寸，策略计算的交易将因初始偏移而无法正常工作。\n交易 # 在使用方面没有变化。只需使用策略中提供的方法（请参阅策略参考以获取完整说明）：\nbuy sell close cancel 返回的订单对象 # 与 backtrader 订单对象兼容（在相同层次结构中子类化）。\n订单执行类型 # IB 支持多种执行类型，部分由 IB 模拟，部分由交易所支持。选择支持哪些类型的动机是：\n与 backtrader 模拟经纪商兼容\n这样，上线的内容都经过了回测验证。\n因此，订单执行类型限于模拟经纪商中可用的类型：\nOrder.Market Order.Close Order.Limit Order.Stop（触发止损后跟随一个市价单） Order.StopLimit（触发止损后跟随一个限价单） 注意：IB 根据策略触发止损。backtrader 不修改默认设置（0）：\n0 - 默认值。对于 OTC 股票和美国期权，使用”双重买/卖”方法；其他订单使用”最后”方法。 如需修改此设置，可以按照 IB 文档传入额外的 **kwargs。例如，在策略的 next 方法中：\ndef next(self): # 一些逻辑 self.buy(data, m_triggerMethod=2) 这将策略改为 2（“最后”方法，其中止损订单基于最后价格触发）。\n请参考 IB API 文档以获取关于止损触发的进一步说明。\n订单有效期 # 回测中使用的有效期概念（valid 参数）在实时交易中同样适用。有效期参数转换为 IB 订单的方式如下：\nNone -\u0026gt; GTC（Good Til Cancelled）\n未指定有效期，表示订单有效直至取消。\ndatetime/date -\u0026gt; GTD（Good Til Date）\n传入 datetime.datetime 或 datetime.date 实例表示订单有效至指定时间点。\ntimedelta(x) -\u0026gt; GTD（timedelta(x) 不等于 timedelta()）\n表示订单有效期为当前时间加上 timedelta(x)。\nfloat -\u0026gt; GTD\n如果值来自 backtrader 的浮点日期时间存储，订单有效至该浮点数指示的时间。\ntimedelta() 或 0 -\u0026gt; DAY\n指定了值（非 None）但为空值，表示当日（当前会话）有效的订单。\n通知 # 标准订单状态通过策略的 notify_order 方法通知（如果覆盖）。\nSubmitted - 订单已发送到 TWS\nAccepted - 订单已被放置\nRejected - 订单放置失败或在其生命周期内被系统取消\nPartial - 部分执行已发生\nCompleted - 订单已完全执行\nCanceled（或 Cancelled）\n在 IB 中有多种含义：\n用户手动取消 服务器/交易所取消订单 订单有效期已过 会应用启发式方法：如果从 TWS 收到 openOrder 消息且状态为 PendingCancel 或 Canceled，订单将被标记为已过期。\nExpired - 参见上文解释。\n参考 # IBStore # class backtrader.stores.IBStore() 包装 ibpy ibConnection 实例的单例类。\n这些参数也可以在使用该存储的类（如 IBData 和 IBBroker）中指定。\n参数：\nhost（默认：127.0.0.1）：IB TWS 或 IB Gateway 的运行地址。通常为 localhost，但不一定。 port（默认：7496）：连接端口。演示系统使用 7497。 clientId（默认：无）：TWS 连接标识。 无：自动生成 1 到 65535 之间的随机 id 整数：直接使用该值。 notifyall（默认：False） 为 False 时，仅将错误消息发送到 notify_store 方法。 为 True 时，转发所有从 TWS 收到的消息。 _debug（默认：False） 将所有收到的 TWS 消息打印到标准输出。 reconnect（默认：3） 初始连接失败后的重试次数。 设为 -1 表示无限重连。 timeout（默认：3.0） 重连间隔（秒）。 timeoffset（默认：True） 为 True 时，使用从 reqCurrentTime（IB 服务器时间）计算与本地时间的偏移，并将该偏移用于价格通知的时间戳修正（如 CASH 市场的 tickPrice 事件）。 时间偏移会传播到其他模块（如重采样），以对齐时间戳。 timerefresh（默认：60.0） 时间偏移刷新间隔（秒）。 indcash（默认：True） 将 IND 代码作为现金资产处理以获取价格。 IBBroker # class backtrader.brokers.IBBroker(**kwargs) 盈透的经纪商实现。\n该类将盈透的订单/头寸映射到 backtrader 的内部 API。\n注意事项：\ntradeid 实际不受支持，因为盈亏直接从盈透获取，以 FIFO 方式计算，导致 tradeid 的盈亏不准确。 头寸：如果启动时已有未平仓头寸，或通过其他方式改变了头寸，Cerebro 中的策略交易将不反映实际情况。 若要避免此问题，经纪商需自行管理头寸（这也支持多个 tradeid 的盈亏本地计算），但这可能违背使用实时经纪商的初衷。 IBData # class backtrader.feeds.IBData(**kwargs) 盈透数据馈送。\ndataname 参数支持以下合约规格：\nTICKER # 股票类型和 SMART 交易所 TICKER-STK # 股票和 SMART 交易所 TICKER-STK-EXCHANGE # 股票 TICKER-STK-EXCHANGE-CURRENCY # 股票 TICKER-CFD # CFD 和 SMART 交易所 TICKER-CFD-EXCHANGE # CFD TICKER-CDF-EXCHANGE-CURRENCY # 股票 TICKER-IND-EXCHANGE # 指数 TICKER-IND-EXCHANGE-CURRENCY # 指数 TICKER-YYYYMM-EXCHANGE # 期货 `TICKER-YYYY MM-EXCHANGE-CURRENCY` # 期货\nTICKER-YYYYMM-EXCHANGE-CURRENCY-MULT # 期货 TICKER-FUT-EXCHANGE-CURRENCY-YYYYMM-MULT # 期货 TICKER-YYYYMM-EXCHANGE-CURRENCY-STRIKE-RIGHT # 期权 TICKER-YYYYMM-EXCHANGE-CURRENCY-STRIKE-RIGHT-MULT # 期权 TICKER-FOP-EXCHANGE-CURRENCY-YYYYMM-STRIKE-RIGHT # 期权 TICKER-FOP-EXCHANGE-CURRENCY-YYYYMM-STRIKE-RIGHT-MULT # 期权 CUR1.CUR2-CASH-IDEALPRO # 外汇 TICKER-YYYYMMDD-EXCHANGE-CURRENCY-STRIKE-RIGHT # 期权 TICKER-YYYYMMDD-EXCHANGE-CURRENCY-STRIKE-RIGHT-MULT # 期权 TICKER-OPT-EXCHANGE-CURRENCY-YYYYMMDD-STRIKE-RIGHT # 期权 TICKER-OPT-EXCHANGE-CURRENCY-YYYYMMDD-STRIKE-RIGHT-MULT # 期权 参数：\nsectype（默认：STK）\n在 dataname 中未指定时应用的默认证券类型。 exchange（默认：SMART）\n在 dataname 中未指定时应用的默认交易所。 currency（默认：\u0026rsquo;\u0026rsquo;）\n在 dataname 中未指定时应用的默认货币。 historical（默认：False）\n设为 True 时，数据馈送在首次下载数据后停止。 使用 fromdate 和 todate 作为参考。 如果请求持续时间超过 IB 在给定时间框架/压缩下的限制，将自动分多次请求。 what（默认：None）\n为 None 时，历史数据请求的默认值因资产类型而异： CASH 资产：\u0026lsquo;BID\u0026rsquo; 其他：\u0026lsquo;TRADES\u0026rsquo; 如需其他值，请查阅 IB API 文档。 rtbar（默认：False）\n为 True 时，使用盈透提供的 5 秒实时条作为最小刻度。这些条形图对应经过 IB 处理后的实时值。 为 False 时，使用基于接收 tick 的 RTVolume 价格。CASH 资产（如 EUR.JPY）始终使用 RTVolume，从中提取买入价格（这已成为与 IB 协作的行业默认做法）。 即使设为 True，如果数据重采样到低于 Seconds/5 的时间框架，也不会使用实时条，因为 IB 不提供更细粒度。 qcheck（默认：0.5）\n无数据接收时的唤醒间隔（秒），用于重采样/重放和通知传递。 backfill_start（默认：True）\n启动时执行回填。通过单个请求获取最大历史数据量。 backfill（默认：True）\n断开/重连后执行回填。按缺口时长下载最小数据量。 backfill_from（默认：None）\n可用于初始回填的备用数据源。数据源耗尽后，如有需要再从 IB 回填。典型用法是使用本地文件等持久化来源。 latethrough（默认：False）\n数据源被重采样/重放时，部分 tick 可能迟到。设为 True 允许这些延迟 tick 通过。 请查阅重采样器文档了解如何处理这些 tick。 此情况在 timeoffset 设为 False 且 TWS 服务器时间与本地时间不同步时可能发生。 tradename（默认：None）\n某些场景下（如 CFD），价格由一个资产提供，交易却在另一个资产中进行。 SPY-STK-SMART-USD -\u0026gt; SP500 ETF（将指定为 `dataname`） SPY-CFD-SMART-USD -\u0026gt; 对应的 CFD，不提供价格跟踪，但在这种情况下将是交易资产（指定为 `tradename`）。 默认参数允许 TICKER 这样的简写，其中 sectype（STK）和 exchange（SMART）使用默认值。\n部分资产（如 AAPL）需要完整的规格说明（包含货币），而其他资产（如 TWTR）可以直接使用简写。\n完整规格示例：dataname='AAPL-STK-SMART-USD'\n或者：IBData(dataname='AAPL', currency='USD') 使用默认的 STK 和 SMART，仅覆盖货币为 USD。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/15-livetrading/01-interactive-brokers/","section":"教程","summary":"与盈透（Interactive Brokers）的集成支持以下功能：\n实时数据馈送 实时交易 注意：虽然已尽力测试了各种错误条件，但代码（像任何软件一样）仍可能包含错误。上线前，请使用模拟交易账户或 TWS 演示账户充分测试策略。\n","title":"Interactive Brokers 实盘接入","type":"docs"},{"content":"注意，截至 2017-07-25，pyfolio 的 API 已更改，create_full_tear_sheet 不再接受 gross_lev 作为命名参数。因此，集成示例无法正常工作。\n引用 pyfolio 主页面 http://quantopian.github.io/pyfolio/ 的内容： pyfolio 是一个由 Quantopian Inc. 开发的金融投资组合表现和风险分析 Python 库。它与开源回测库 Zipline 配合良好，现在也能很好地与 backtrader 配合。所需内容包括：\n需要 pyfolio 以及它的依赖项（如 pandas、seaborn 等） 注意，在与 0.5.1 版本集成期间，需要更新依赖项，例如将 seaborn 从 0.7.0-dev 更新到 0.7.1，因为缺少 swarmplot 方法。\n用法 # 将 PyFolio 分析器添加到 cerebro 中：\ncerebro.addanalyzer(bt.analyzers.PyFolio) 运行并检索第一个策略：\nstrats = cerebro.run() strat0 = strats[0] 使用你给分析器指定的名称或默认名称（pyfolio）来检索分析器。例如：\npyfolio = strats.analyzers.getbyname(\u0026#39;pyfolio\u0026#39;) 使用分析器的 get_pf_items 方法检索 pyfolio 需要的四个组件：\nreturns, positions, transactions, gross_lev = pyfolio.get_pf_items() 注意\n集成是通过查看 pyfolio 的测试样本并复制相同的标题来实现的。\n与 pyfolio 协同工作（已超出 backtrader 生态系统范围）\n一些与 backtrader 无关的使用注意事项：\npyfolio 自动绘图在 Jupyter Notebook 之外也能工作，但在 Notebook 内部效果最佳。 pyfolio 数据表的输出在 Jupyter Notebook 之外几乎无法使用，但在 Notebook 内部可以正常工作。 结论很简单：如果希望使用 pyfolio，请在 Jupyter Notebook 中操作。\n示例代码 代码看起来像这样：\n... cerebro.addanalyzer(bt.analyzers.PyFolio, _name=\u0026#39;pyfolio\u0026#39;) ... results = cerebro.run() strat = results[0] pyfoliozer = strat.analyzers.getbyname(\u0026#39;pyfolio\u0026#39;) returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items() ... ... # pyfolio showtime import pyfolio as pf pf.create_full_tear_sheet( returns, positions=positions, transactions=transactions, gross_lev=gross_lev, live_start_date=\u0026#39;2005-05-01\u0026#39;, # 此日期是样本特定的 round_trips=True) # 此时表格和图表将显示 参考 # 查看分析器参考以了解 PyFolio 分析器及其内部使用的分析器。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/12-analyzers/02-pyfolio/","section":"教程","summary":"注意，截至 2017-07-25，pyfolio 的 API 已更改，create_full_tear_sheet 不再接受 gross_lev 作为命名参数。因此，集成示例无法正常工作。\n","title":"Pyfolio 绩效分析库","type":"docs"},{"content":" 本文是 Backtrader 量化交易框架的安装指南。我们将详细介绍多种安装方式，并涵盖版本兼容性、功能依赖和验证安装的完整流程。\n环境与兼容性 # Backtrader 官方支持 Python 3.2-3.7，但实际测试表明，它在 Python 3.10 乃至最新的 3.13 版本上都能稳定运行。\n虽然官方主要基于 Python 2.7 进行开发，并在 3.4 环境下测试，但当前版本对新版 Python 的兼容性表现良好。如需使用绘图功能，请确保安装 Matplotlib 1.4.1 或更高版本。\n安装方式 # 最便捷的方式是通过 PyPI 进行安装。只需执行以下命令即可完成基础安装：\npip install backtrader 如果需要使用绘图功能，建议安装包含 Matplotlib 的完整版本：\npip install \u0026#34;backtrader[plotting]\u0026#34; 这条命令会自动安装 Matplotlib 及其所有依赖项，省去手动配置的麻烦。\n验证安装效果 # 为了确认安装成功，我们可以运行一个简单的测试策略。以下是完整的测试代码示例：\nimport yfinance as yf import backtrader as bt from datetime import datetime class BuyHold(bt.Strategy): def next(self): if not self.position: self.buy() cerebro = bt.Cerebro() cerebro.addstrategy(BuyHold) raw_data = yf.download( \u0026#34;AAPL\u0026#34;, start=\u0026#34;2020-01-01\u0026#34;, end=\u0026#34;2021-01-01\u0026#34;, multi_level_index=False ) apple_data = bt.feeds.PandasData(dataname=raw_data) cerebro.adddata(apple_data) cerebro.broker.setcash(1e8) print(f\u0026#34;初始资金: {cerebro.broker.getcash()}\u0026#34;) cerebro.run() print(f\u0026#34;最终资金: {cerebro.broker.getcash()}\u0026#34;) cerebro.plot() 运行前请确保已安装 yfinance 库：\npip install yfinance 执行脚本：\npython buyhold.py 如果能看到资金变化并显示交易图表，说明安装完全成功。\n源码安装方式 # 对于希望深入研究的用户，可以从 GitHub 获取最新源码：\ngit clone https://github.com:mementum/backtrader src cp -r src/backtrader project_directory/ cd project_directory export PYTHONPATH=`pwd` 这种方式虽然需要手动安装 Matplotlib，但便于调试和阅读核心代码，特别适合需要进行深度定制的用户。\n额外提示 # 如果追求极致的回测性能，可以尝试在 PyPy3 环境下运行 Backtrader，不过需要注意其绘图功能支持相对有限。无论选择哪种安装方式，都建议通过测试代码验证各项功能是否正常。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/01-intro-install/02-installation/","section":"教程","summary":" 本文是 Backtrader 量化交易框架的安装指南。我们将详细介绍多种安装方式，并涵盖版本兼容性、功能依赖和验证安装的完整流程。\n","title":"安装指南与环境配置","type":"docs"},{"content":" FixedSize # class backtrader.sizers.FixedSize() 此 Sizer 为任何操作返回固定数量。通过指定 tranches 参数，可以控制逐步进入交易的批次数量。\n参数：\nstake（默认：1） tranches（默认：1） FixedReverser # class backtrader.sizers.FixedReverser() 此 Sizer 返回反转已开头寸或开仓所需的固定数量。\n开仓：返回参数 stake 反转头寸：返回 2 * stake 参数：\nstake（默认：1） PercentSizer # class backtrader.sizers.PercentSizer() 此 Sizer 返回可用现金的百分比。\n参数：\npercents（默认：20） AllInSizer # class backtrader.sizers.AllInSizer() 此 Sizer 返回经纪商的所有可用现金。\n参数：\npercents（默认：100） PercentSizerInt # class backtrader.sizers.PercentSizerInt() 此 Sizer 以整数形式返回可用现金的百分比，并将大小截断为整数。\n参数：\npercents（默认：20） AllInSizerInt # class backtrader.sizers.AllInSizerInt() 此 Sizer 返回经纪商的所有可用现金，并将数量截断为整数。\n参数：\npercents（默认：100） ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/14-sizers/02-reference/","section":"教程","summary":"FixedSize # class backtrader.sizers.FixedSize() 此 Sizer 为任何操作返回固定数量。通过指定 tranches 参数，可以控制逐步进入交易的批次数量。\n参数：\n","title":"仓位管理 API 参考文档","type":"docs"},{"content":"对于订单管理，Backtrader 提供了3种基本操作：\nbuy sell cancel 注意： 更新操作看起来合乎逻辑，但这种方式主要用于手动交易者。\n对于订单执行逻辑，提供以下执行类型：\n市价订单（Market） 收盘价订单（Close） 限价订单（Limit） 止损订单（Stop） 止损限价订单（StopLimit） 订单管理 # 一些示例：\n# 购买主数据，使用sizer的默认股份，市价订单 order = self.buy() # 市价订单 - 有效期将被 \u0026#34;忽略\u0026#34; order = self.buy(valid=datetime.datetime.now() + datetime.timedelta(days=3)) # 市价订单 - 价格将被忽略 order = self.buy(price=self.data.close[0] * 1.02) # 市价订单 - 手动股份 order = self.buy(size=25) # 限价订单 - 想要设置价格并可以设置有效期 order = self.buy(exectype=Order.Limit, price=self.data.close[0] * 1.02, valid=datetime.datetime.now() + datetime.timedelta(days=3)) # 止损限价订单 - 想要设置价格，价格限制 order = self.buy(exectype=Order.StopLimit, price=self.data.close[0] * 1.02, plimit=self.data.close[0] * 1.07) # 取消现有订单 self.broker.cancel(order) 注意：\n所有订单类型都可以通过创建一个订单实例（或其子类之一）并传递给 broker 来创建：\norder = self.broker.submit(order) 注意：\nBroker 中也有 buy 和 sell 操作，但它们对默认参数的容忍度较低。\n订单执行逻辑 # Broker 使用以下原则进行订单执行。\n原则一 # 当前数据已经发生，不能用于执行订单。\n如果策略中有如下逻辑：\nif self.data.close \u0026gt; self.sma: # 其中 sma 是简单移动平均线 self.buy() 不能期望订单以逻辑中检查的收盘价执行，因为该价格已经过去。\n原则二 # 订单在下一组开盘/最高/最低/收盘价格范围内执行（还需满足订单中设置的条件）。\n原则三 # 成交量在回测中不起作用。实际交易中，如果选择非流动性资产或恰好触及价格栏的高/低点，成交量才会真正起作用。\n但触及高/低点的情况很少发生（如果发生……你不需要 backtrader），所选资产应有足够的流动性来吸收正常交易订单。\n市价订单（Market） # 执行：\n下一个价格栏的开盘价（通常称为 bar） 理由：\n如果在时间点 X 执行逻辑并发出市价订单，下一个价格点是即将到来的开盘价。 注意：\n该订单始终执行，创建时指定的价格和有效期参数将被忽略。 收盘价订单（Close） # 执行：\n使用下一个价格栏的收盘价，当该价格栏真正关闭时。 理由：\n大多数回测数据源已包含已关闭的价格栏，订单会立即以下一个价格栏的收盘价执行（如日线数据）。 但如果系统接收 tick 数据，价格栏会不断被新 tick 更新，不会切换到下一个价格栏（因为时间/日期未改变）。 只有当时间或日期改变时，价格栏才真正关闭，订单才执行。 限价订单（Limit） # 执行：\n如果数据触及创建时设置的价格，则从下一个价格栏开始执行。 如果设置了有效期并到期，订单将被取消。 价格匹配：\nbacktrader 尝试为限价订单提供最符合实际的执行价格。 通过开盘价/最高价/最低价/收盘价 4 个价格点，可以推断请求的价格是否可被改善。 对于买入订单： 情况1：如果开盘价低于限价，订单立即以开盘价执行。 情况2：如果开盘价不低于限价，但最低价低于限价，说明限价在交易期间已出现，订单可以执行。 卖出订单逻辑相反。 止损订单（Stop） # 执行：\n如果数据触及创建时设置的触发价格，则从下一个价格栏开始执行。 如果设置了有效期并到期，订单将被取消。 价格匹配：\nbacktrader 尝试为止损订单提供最符合实际的触发价格。 通过开盘价/最高价/最低价/收盘价 4 个价格点，可以推断请求的价格是否可被触发。 对于买入止损订单： 情况1：如果开盘价高于止损价，订单立即以开盘价执行，防止价格上涨对现有空头头寸造成更大亏损。 情况2：如果开盘价不高于止损价，但最高价高于止损价，说明止损价在交易期间已出现，订单可以执行。 卖出订单逻辑相反。 止损限价订单（StopLimit） # 执行：\n触发价格从下一个价格栏开始启动订单。 价格匹配：\n触发：使用止损匹配逻辑（但仅触发并将订单转变为限价订单）。 限价：使用限价匹配逻辑。 示例 # 图片（带代码）胜过千言万语。以下代码段主要展示订单创建部分，完整代码在底部。\n策略使用价格相对于简单移动平均线的位置来生成买入/卖出信号。\n信号在图表底部可见（使用交叉指示器）。\n策略会保存”买入”订单的引用，确保系统中最多同时存在一个订单。\n市价订单（Market） # 从图表可以看到，订单在信号生成后的下一根价格栏以开盘价执行。\nif self.p.exectype == \u0026#39;Market\u0026#39;: self.buy(exectype=bt.Order.Market) # 如果未给定，则默认 self.log(\u0026#39;BUY CREATE, exectype Market, price %.2f\u0026#39; % self.data.close[0]) 收盘价订单（Close） # 订单在信号生成后的下一根价格栏以收盘价执行。\nelif self.p.exectype == \u0026#39;Close\u0026#39;: self.buy(exectype=bt.Order.Close) self.log(\u0026#39;BUY CREATE, exectype Close, price %.2f\u0026#39; % self.data.close[0]) 执行类型：限价订单（Limit） # 有效期在传递为参数时会提前计算。\nif self.p.valid: valid = self.data.datetime.date(0) + datetime.timedelta(days=self.p.valid) else: valid = None 设置一个比信号生成价格（信号栏的收盘价）低1%的限价，这导致很多订单无法执行。\nelif self.p.exectype == \u0026#39;Limit\u0026#39;: price = self.data.close * (1.0 - self.p.perc1 / 100.0) self.buy(exectype=bt.Order.Limit, price=price, valid=valid) if self.p.valid: txt = \u0026#39;BUY CREATE, exectype Limit, price %.2f, valid: %s\u0026#39; self.log(txt % (price, valid.strftime(\u0026#39;%Y-%m-%d\u0026#39;))) else: txt = \u0026#39;BUY CREATE, exectype Limit, price %.2f\u0026#39; self.log(txt % price) 带有效期的限价订单 # 为了避免无限等待一个可能不利的限价订单，订单有效期设为 4 个自然日。\n$ ./order-execution-samples.py --exectype Limit --perc1 1 --valid 4 止损订单（Stop） # 设置一个比信号价格高1%的止损价格。这意味着策略等待信号生成后价格继续上涨才买入，可视为一种确认信号。\nelif self.p.exectype == \u0026#39;Stop\u0026#39;: price = self.data.close * (1.0 + self.p.perc1 / 100.0) self.buy(exectype=bt.Order.Stop, price=price, valid=valid) if self.p.valid: txt = \u0026#39;BUY CREATE, exectype Stop, price %.2f, valid: %s\u0026#39; self.log(txt % (price, valid.strftime(\u0026#39;%Y-%m-%d\u0026#39;))) else: txt = \u0026#39;BUY CREATE, exectype Stop, price %.2f\u0026#39; self.log(txt % price) 止损限价订单（StopLimit） # 设置一个比信号价格高1%的止损价格，但限价设置为比信号价格高0.5%。意思是：等待强劲信号，但不追高，等待回落。\nelif self.p.exectype == \u0026#39;StopLimit\u0026#39;: price = self.data.close * (1.0 + self.p.perc1 / 100.0) plimit = self.data.close * (1.0 + self.p.perc2 / 100.0) self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid, plimit=plimit) if self.p.valid: txt = (\u0026#39;BUY CREATE, exectype StopLimit, price %.2f,\u0026#39; \u0026#39; valid: %s, pricelimit: %.2f\u0026#39;) self.log(txt % (price, valid.strftime(\u0026#39;%Y-%m-%d\u0026#39;), plimit)) else: txt = (\u0026#39;BUY CREATE, exectype StopLimit, price %.2f,\u0026#39; \u0026#39; pricelimit: %.2f\u0026#39;) self.log(txt % (price, plimit)) 测试脚本执行 # 详见命令行帮助：\n$ ./order-execution-samples.py --help usage: order-execution-samples.py [-h] [--infile INFILE] [--csvformat {bt,visualchart,sierrachart,yahoo,yahoo_unreversed}] [--fromdate FROMDATE] [--todate TODATE] [--plot] [--plotstyle {bar,line,candle}] [--numfigs NUMFIGS] [--smaperiod SMAPERIOD] [--exectype EXECTYPE] [--valid VALID] [--perc1 PERC1] [--perc2 PERC2] 完整代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import os.path import time import sys import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class OrderExecutionStrategy(bt.Strategy): params = ( (\u0026#39;smaperiod\u0026#39;, 15), (\u0026#39;exectype\u0026#39;, \u0026#39;Market\u0026#39;), (\u0026#39;perc1\u0026#39;, 3), (\u0026#39;perc2\u0026#39;, 1), (\u0026#39;valid\u0026#39;, 4), ) def log(self, txt, dt=None): \u0026#39;\u0026#39;\u0026#39; Logging function for this strategy\u0026#39;\u0026#39;\u0026#39; dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do self.log(\u0026#39;ORDER ACCEPTED/SUBMITTED\u0026#39;, dt=order.created.dt) self.order = order return if order.status in [order.Expired]: self.log(\u0026#39;BUY EXPIRED\u0026#39;) elif order.status in [order.Completed]: if order.isbuy(): self.log( \u0026#39;BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) else: # Sell self.log(\u0026#39;SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) # Sentinel to None: new orders allowed self.order = None def __init__(self): # SimpleMovingAverage on main data # Equivalent to -\u0026gt; sma = btind.SMA(self.data, period=self.p.smaperiod) sma = btind.SMA(period=self.p.smaperiod) # CrossOver (1: up, -1: down) close / sma self.buysell = btind.CrossOver(self.data.close, sma, plot=True) # Sentinel to None: new orders allowed self.order = None def next(self): if self.order: # An order is pending ... nothing can be done return # Check if we are in the market if self.position: # In the market - check if it\u0026#39;s the time to sell if self.buysell \u0026lt; 0: self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.data.close[0]) self.sell() elif self.buysell \u0026gt; 0: if self.p.valid: valid = self.data.datetime.date(0) + \\ datetime.timedelta(days=self.p.valid) else: valid = None # Not in the market and signal to buy if self.p.exectype == \u0026#39;Market\u0026#39;: self.buy(exectype=bt.Order.Market) # default if not given self.log(\u0026#39;BUY CREATE, exectype Market, price %.2f\u0026#39; % self.data.close[0]) elif self.p.exectype == \u0026#39;Close\u0026#39;: self.buy(exectype=bt.Order.Close) self.log(\u0026#39;BUY CREATE, exectype Close, price %.2f\u0026#39; % self.data.close[0]) elif self.p.exectype == \u0026#39;Limit\u0026#39;: price = self.data.close * (1.0 - self.p.perc1 / 100.0) self.buy(exectype=bt.Order.Limit, price=price, valid=valid) if self.p.valid: txt = \u0026#39;BUY CREATE, exectype Limit, price %.2f, valid: %s\u0026#39; self.log(txt % (price, valid.strftime(\u0026#39;%Y-%m-%d\u0026#39;))) else: txt = \u0026#39;BUY CREATE, exectype Limit, price %.2f\u0026#39; self.log(txt % price) elif self.p.exectype == \u0026#39;Stop\u0026#39;: price = self.data.close * (1.0 + self.p.perc1 / 100.0) self.buy(exectype=bt.Order.Stop, price=price, valid=valid) if self.p.valid: txt = \u0026#39;BUY CREATE, exectype Stop, price %.2f, valid: %s\u0026#39; self.log(txt % (price, valid.strftime(\u0026#39;%Y-%m-%d\u0026#39;))) else: txt = \u0026#39;BUY CREATE, exectype Stop, price %.2f\u0026#39; self.log(txt % price) elif self.p.exectype == \u0026#39;StopLimit\u0026#39;: price = self.data.close * (1.0 + self.p.perc1 / 100.0) plimit = self.data.close * (1.0 + self.p.perc2 / 100.0) self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid, plimit=plimit) if self.p.valid: txt = (\u0026#39;BUY CREATE, exectype StopLimit, price %.2f,\u0026#39; \u0026#39; valid: %s, pricelimit: %.2f\u0026#39;) self.log(txt % (price, valid.strftime(\u0026#39;%Y-%m-%d\u0026#39;), plimit)) else: txt = (\u0026#39;BUY CREATE, exectype StopLimit, price %.2f,\u0026#39; \u0026#39; pricelimit: %.2f\u0026#39;) self.log(txt % (price, plimit)) def runstrat(): args = parse_args() cerebro = bt.Cerebro() data = getdata(args) cerebro.adddata(data) cerebro.addstrategy( OrderExecutionStrategy, exectype=args.exectype, perc1=args.perc1, perc2=args.perc2, valid=args.valid, smaperiod=args.smaperiod ) cerebro.run() if args.plot: cerebro.plot(numfigs=args.numfigs, style=args.plotstyle) def getdata(args): dataformat = dict( bt=btfeeds.BacktraderCSVData, visualchart=btfeeds.VChartCSVData, sierrachart=btfeeds.SierraChartCSVData, yahoo=btfeeds.YahooFinanceCSVData, yahoo_unreversed=btfeeds.YahooFinanceCSVData ) dfkwargs = dict() if args.csvformat == \u0026#39;yahoo_unreversed\u0026#39;: dfkwargs[\u0026#39;reverse\u0026#39;] = True if args.fromdate: fromdate = datetime.datetime.strptime(args.fromdate, \u0026#39;%Y-%m-%d\u0026#39;) dfkwargs[\u0026#39;fromdate\u0026#39;] = fromdate if args.todate: fromdate = datetime.datetime.strptime(args.todate, \u0026#39;%Y-%m-%d\u0026#39;) dfkwargs[\u0026#39;todate\u0026#39;] = todate dfkwargs[\u0026#39;dataname\u0026#39;] = args.infile dfcls = dataformat[args.csvformat] return dfcls(**dfkwargs) def parse_args(): parser = argparse.ArgumentParser( description=\u0026#39;Showcase for Order Execution Types\u0026#39;) parser.add_argument(\u0026#39;--infile\u0026#39;, \u0026#39;-i\u0026#39;, required=False, default=\u0026#39;../../datas/2006-day-001.txt\u0026#39;, help=\u0026#39;File to be read in\u0026#39;) parser.add_argument(\u0026#39;--csvformat\u0026#39;, \u0026#39;-c\u0026#39;, required=False, default=\u0026#39;bt\u0026#39;, choices=[\u0026#39;bt\u0026#39;, \u0026#39;visualchart\u0026#39;, \u0026#39;sierrachart\u0026#39;, \u0026#39;yahoo\u0026#39;, \u0026#39;yahoo_unreversed\u0026#39;], help=\u0026#39;CSV Format\u0026#39;) parser.add_argument(\u0026#39;--fromdate\u0026#39;, \u0026#39;-f\u0026#39;, required=False, default=None, help=\u0026#39;Starting date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, \u0026#39;-t\u0026#39;, required=False, default=None, help=\u0026#39;Ending date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, \u0026#39;-p\u0026#39;, action=\u0026#39;store_false\u0026#39;, required=False, help=\u0026#39;Plot the read data\u0026#39;) parser.add_argument(\u0026#39;--plotstyle\u0026#39;, \u0026#39;-ps\u0026#39;, required=False, default=\u0026#39;bar\u0026#39;, choices=[\u0026#39;bar\u0026#39;, \u0026#39;line\u0026#39;, \u0026#39;candle\u0026#39;], help=\u0026#39;Plot the read data\u0026#39;) parser.add_argument(\u0026#39;--numfigs\u0026#39;, \u0026#39;-n\u0026#39;, required=False, default=1, help=\u0026#39;Plot using n figures\u0026#39;) parser.add_argument(\u0026#39;--smaperiod\u0026#39;, \u0026#39;-s\u0026#39;, required=False, default=15, help=\u0026#39;Simple Moving Average Period\u0026#39;) parser.add_argument(\u0026#39;--exectype\u0026#39;, \u0026#39;-e\u0026#39;, required=False, default=\u0026#39;Market\u0026#39;, help=(\u0026#39;Execution Type: Market (default), Close, Limit,\u0026#39; \u0026#39; Stop, StopLimit\u0026#39;)) parser.add_argument(\u0026#39;--valid\u0026#39;, \u0026#39;-v\u0026#39;, required=False, default=0, type=int, help=\u0026#39;Validity for Limit sample: default 0 days\u0026#39;) parser.add_argument(\u0026#39;--perc1\u0026#39;, \u0026#39;-p1\u0026#39;, required=False, default=0.0, type=float, help=(\u0026#39;%% distance from close price at order creation\u0026#39; \u0026#39; time for the limit/trigger price in Limit/Stop\u0026#39; \u0026#39; orders\u0026#39;)) parser.add_argument(\u0026#39;--perc2\u0026#39;, \u0026#39;-p2\u0026#39;, required=False, default=0.0, type=float, help=(\u0026#39;%% distance from close price at order creation\u0026#39; \u0026#39; time for the limit price in StopLimit orders\u0026#39;)) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/09-orders/02-creation-execution/","section":"教程","summary":"对于订单管理，Backtrader 提供了3种基本操作：\nbuy sell cancel 注意： 更新操作看起来合乎逻辑，但这种方式主要用于手动交易者。\n对于订单执行逻辑，提供以下执行类型：\n","title":"订单创建与执行流程","type":"docs"},{"content":"在版本 1.9.44.116 中，backtrader 添加了定时器功能，可以在特定时间点调用 notify_timer（在 Cerebro 和 Strategy 中可用），支持精细控制。\n选项 # 基于绝对时间或相对于会话开始/结束时间的定时器。 时区指定，可通过 pytz 兼容对象或数据源会话时间指定。 相对于指定时间的起始偏移量。 重复间隔。 工作日过滤器（支持结转）。 月份天数过滤器（支持结转）。 自定义回调过滤器。 使用模式 # 在 Cerebro 和 Strategy 子类中，定时器回调通过以下方法接收：\ndef notify_timer(self, timer, when, *args, **kwargs): \u0026#39;\u0026#39;\u0026#39;接收定时器通知，其中 `timer` 是通过 `add_timer` 返回的定时器实例，`when` 是调用时间。 `args` 和 `kwargs` 是传递给 `add_timer` 的额外参数。 实际的 `when` 时间可能稍有延迟，因为系统可能无法及时调用。 此值是定时器的目标时间，而非系统时间。 \u0026#39;\u0026#39;\u0026#39; 添加定时器 - 通过 Strategy # 通过以下方法添加：\ndef add_timer(self, when, offset=datetime.timedelta(), repeat=datetime.timedelta(), weekdays=[], weekcarry=False, monthdays=[], monthcarry=True, allow=None, tzdata=None, cheat=False, *args, **kwargs): \u0026#39;\u0026#39;\u0026#39; 返回创建的 Timer 实例。参数说明见下文。\n添加定时器 - 通过 Cerebro # 使用相同的方法，只是增加了参数 strats。如果设置为 True，定时器不仅会通知给 cerebro，还会通知系统中运行的所有策略。\ndef add_timer(self, when, offset=datetime.timedelta(), repeat=datetime.timedelta(), weekdays=[], weekcarry=False, monthdays=[], monthcarry=True, allow=None, tzdata=None, cheat=False, strats=False, *args, **kwargs): \u0026#39;\u0026#39;\u0026#39; 它返回创建的 Timer 实例。\n定时器调用时间 # cheat=False（默认） # 定时器在以下步骤后调用：\n数据源加载当前 K 线的新值后。 经纪人评估订单并重新计算投资组合价值后。 指标重新计算前（由策略触发）。 调用策略的 next 方法前。 cheat=True # 定时器在以下步骤后调用：\n数据源加载当前 K 线的新值后。 经纪人评估订单并重新计算投资组合价值前。 即定时器在指标重新计算和调用 next 之前触发，可实现以下场景：\n在新 K 线被经纪人评估前调用定时器。 指标仍使用前一日收盘值，可用于生成进出信号。 新价格可用后，使用开盘价计算下单数量。 使用每日柱 # 示例 scheduled.py 默认使用 backtrader 发行版中提供的标准每日柱。策略参数如下：\nclass St(bt.Strategy): params = dict( when=bt.timer.SESSION_START, timer=True, cheat=False, offset=datetime.timedelta(), repeat=datetime.timedelta(), weekdays=[], ) 数据具有以下会话时间：\n开始：09:00 结束：17:30 仅设置时间的情况：\n$ ./scheduled.py --strat when=\u0026#39;datetime.time(15,30)\u0026#39; 输出结果：\nstrategy notify_timer with tid 0, when 2005-01-03 15:30:00 cheat False 1, 2005-01-03 17:30:00, Week 1, Day 1, O 2952.29, H 2989.61, L 2946.8, C 2970.02 strategy notify_timer with tid 0, when 2005-01-04 15:30:00 cheat False 2, 2005-01-04 17:30:00, Week 1, Day 2, O 2969.78, H 2979.88, L 2961.14, C 2971.12 strategy notify_timer with tid 0, when 2005-01-05 15:30:00 cheat False 3, 2005-01-05 17:30:00, Week 1, Day 3, O 2969.0, H 2969.0, L 2942.69, C 2947.19 strategy notify_timer with tid 0, when 2005-01-06 15:30:00 cheat False ... 定时器在 15:30 触发。没有意外。接下来增加 30 分钟偏移：\n$ ./scheduled.py --strat when=\u0026#39;datetime.time(15,30)\u0026#39;,offset=\u0026#39;datetime.timedelta(minutes=30)\u0026#39; 输出结果：\nstrategy notify_timer with tid 0, when 2005-01-03 16:00:00 cheat False 1, 2005-01-03 17:30:00, Week 1, Day 1, O 2952.29, H 2989.61, L 2946.8, C 2970.02 strategy notify_timer with tid 0, when 2005-01-04 16:00:00 cheat False 2, 2005-01-04 17:30:00, Week 1, Day 2, O 2969.78, H 2979.88, L 2961.14, C 2971.12 strategy notify_timer with tid 0, when 2005-01-05 16:00:00 cheat False ... 定时器从 15:30 变为 16:00。没有意外。接下来以会话开始为参考：\n$ ./scheduled.py --strat when=\u0026#39;bt.timer.SESSION_START\u0026#39;,offset=\u0026#39;datetime.timedelta(minutes=30)\u0026#39; 输出结果：\nstrategy notify_timer with tid 0, when 2005-01-03 09:30:00 cheat False 1, 2005-01-03 17:30:00, Week 1, Day 1, O 2952.29, H 2989.61, L 2946.8, C 2970.02 strategy notify_timer with tid 0, when 2005-01-04 09:30:00 cheat False 2, 2005-01-04 17:30:00, Week 1, Day 2, O 2969.78, H 2979.88, L 2961.14, C 2971.12 ... 定时器回调时间为 09:30。会话开始时间为 09:00，这意味着希望在会话开始后 30 分钟执行操作。\n运行带有 5 分钟柱的数据 # 示例 scheduled-min.py 默认运行 backtrader 发行版中的标准 5 分钟柱。策略参数如下：\nclass St(bt.Strategy): params = dict( when=bt.timer.SESSION_START, timer=True, cheat=False, offset=datetime.timedelta(), repeat=datetime.timedelta(), weekdays=[], weekcarry=False, monthdays=[], monthcarry=True, ) 数据具有相同的会话时间：\n开始：09:00 结束：17:30 让我们进行一些实验。首先是一个定时器。\n$ ./scheduled-min.py --strat when=\u0026#39;datetime.time(15, 30)\u0026#39; 输出结果：\n1, 2006-01-02 09:05:00, Week 1, Day 1, O 3578.73, H 3587.88, L 3578.73, C 3582.99 2, 2006-01-02 09:10:00, Week 1, Day 1, O 3583.01, H 3588.4, L 3583.01, C 3588.03 ... 77, 2006-01-02 15:25:00, Week 1, Day 1, O 3599.07, H 3599.68, L 3598.47, C 3599.68 strategy notify_timer with tid 0, when 2006-01-02 15:30:00 cheat False 78, 2006-01-02 15:30:00, Week 1, Day 1, O 3599.64, H 3599.73, L 3599.0, C 3599.67 ... 179, 2006-01-03 15:25:00, Week 1, Day 2, O 3634.72, H 3635.0, L 3634.06, C 3634.87 strategy notify_timer with tid 0, when 2006-01-03 15:30:00 cheat False 180, 2006-01-03 15:30:00, Week 1, Day 2, O 3634.81, H 3634.89, L 3634.04, C 3634.23 ... 定时器在 15:30 触发。日志中显示了前两天中的触发情况。\n增加 15 分钟的重复间隔 # $ ./scheduled-min.py --strat when=\u0026#39;datetime.time(15, 30)\u0026#39;,repeat=\u0026#39;datetime.timedelta(minutes=15)\u0026#39; 输出结果：\n... 74, 2006-01-02 15:10:00, Week 1, Day 1, O 3596.12, H 3596.63, L 3595.92, C 3596.63 75, 2006-01-02 15:15:00, Week 1, Day 1, O 3596.36, H 3596.65, L 3596.19, C 3596.65 76, 2006-01-02 15:20:00, Week 1, Day 1, O 3596.53, H 3599.13, L 3596.12, C 3598.9 77, 2006-01-02 15:25:00, Week 1, Day 1, O 3599.07, H 3599.68, L 3598.47, C 3599.68 strategy notify_timer with tid 0, when 2006-01-02 15:30:00 cheat False 78, 2006-01-02 15:30:00, Week 1, Day 1, O 3599.64, H 3599.73, L 3599.0, C 3599.67 79, 2006-01-02 15:35:00, Week 1, Day 1, O 3599.61, H 3600.29, L 3599.52, C 3599.92 80, 2006-01-02 15:40:00, Week 1, Day 1, O 3599.96, H 3602.06, L 3599.76, C 3602.05 strategy notify_timer with tid 0, when 2006-01-02 15:45:00 cheat False 81, 2006-01-02 15:45:00, Week 1, Day 1, O 3601.97, H 3602.07, L 3601.45, C 3601.83 82, 2006-01-02 15:50:00, Week 1, Day 1, O 3601.74, H 3602.8, L 3601.63, C 3602.8 83, 2006-01-02 15:55:00, Week 1, Day 1, O 3602.53, H 3602.74, L 3602.33, C 3602.61 strategy notify_timer with tid 0, when 2006-01-02 16:00:00 cheat False 84, 2006-01-02 16:00:00, Week 1, Day 1, O 3602.58, H 3602.75, L 3601.81, C 3602.14 85, 2006-01-02 16:05:00, Week 1, Day 1, O 3602.16, H 3602.16, L 3600.86, C 3600.96 86, 2006-01-02 16:10:00, Week 1, Day 1, O 3601.2, H 3601.49, L 3600.94, C 3601.27 ... strategy notify_timer with tid 0, when 2006-01-02 17:15:00 cheat False 99, 2006-01-02 17:15:00, Week 1, Day 1, O 3603.96, H 3603.96, L 3602.89, C 3603.79 100, 2006-01-02 17:20:00, Week 1, Day 1, O 3603.94, H 3605.95, L 3603.87, C 3603.91 101, 2006-01-02 17:25:00, Week 1, Day 1, O 3604.0, H 3604.76, L 3603.85, C 3604.64 strategy notify_timer with tid 0, when 2006-01-02 17:30:00 cheat False 102, 2006-01-02 17:30:00, Week 1, Day 1, O 3604.06, H 3604.41, L 3603.95, C 3604.33 103, 2006-01-03 09:05:00, Week 1, Day 2, O 3604.08, H 3609.6, L 3604.08, C 3609.6 104, 2006-01-03 09:10:00, Week 1, Day 2, O 3610.34, H 3617.31, L 3610.34, C 3617.31 105, 2006-01-03 09:15:00, Week 1, Day 2, O 3617.61, H 3617.87, L 3616.03, C 3617.51 106, 2006-01-03 09:20:00, Week 1, Day 2, O 3617.24, H 3618.86, L 3616.09, C 3618.42 ... 179, 2006-01-03 15:25:00, Week 1, Day 2, O 3634.72, H 3635.0, L 3634.06, C 3634.87 strategy notify_timer with tid 0, when 2006-01-03 15:30:00 cheat False 180, 2006-01-03 15:30:00, Week 1, Day 2, O 3634.81, H 3634.89, L 3634.04, C 3634.23 ... 使用作弊模式 # $ ./scheduled-min.py --strat when=\u0026#39;bt.timer.SESSION_START\u0026#39;,cheat=True 输出结果：\nstrategy notify_timer with tid 1, when 2006-01-02 09:00:00 cheat True -- 2006-01-02 09:05:00 Create buy order strategy notify_timer with tid 0, when 2006-01-02 09:00:00 cheat False 1, 2006-01-02 09:05:00, Week 1, Day 1, O 3578.73, H 3587.88, L 3578.73, C 3582.99 -- 2006-01-02 09:10:00 Buy Exec @ 3583.01 2, 2006-01-02 09:10:00, Week 1, Day 1, O 3583.01, H 3588.4, L 3583.01, C 3588.03 ... 创建订单时间为 09:05:00，执行时间为 09:10:00，因为经纪人未启用作弊模式。接下来启用它：\n$ ./scheduled-min.py --strat when=\u0026#39;bt.timer.SESSION_START\u0026#39;,cheat=True --broker coo=True 输出结果：\nstrategy notify_timer with tid 1, when 2006-01-02 09:00:00 cheat True -- 2006-01-02 09:05:00 Create buy order strategy notify_timer with tid 0, when 2006-01-02 09:00:00 cheat False -- 2006-01-02 09:05:00 Buy Exec @ 3578.73 1, 2006-01-02 09:05:00, Week 1, Day 1, O 3578.73, H 3587.88, L 3578.73, C 3582.99 2, 2006-01-02 09:10:00, Week 1, Day 1, O 3583.01, H 3588.4, L 3583.01, C 3588.03 ... 下单和执行时间均为 09:05:00，执行价格为 09:05:00 的开盘价。\n其他场景 # 定时器允许通过整数列表指定哪些天可以触发（ISO 代码，周一为 1，周日为 7）。例如：\nweekdays=[5] # 这将请求定时器仅在周五有效 如果周五是非交易日，且定时器应在下一个交易日触发，可添加 weekcarry=True。\n类似地，可以指定每月中的某一天，例如：\nmonthdays=[15] # 每月的 15 日 如果 15 日是非交易日，并且定时器应在下一个交易日触发，可以添加 monthcarry=True。\n目前没有内置功能支持像\u0026quot;3、6、9、12 月的第三个星期五\u0026quot;（期货/期权到期日）这样的规则，但可通过回调函数自定义：\nallow=callable # 回调函数接收一个 datetime.date 实例，如果日期适用于定时器，则返回 True，否则返回 False 实现上述规则的代码：\nclass FutOpExp(object): def __init__(self): self.fridays = 0 self.curmonth = -1 def __call__(self, d): _, _, isowkday = d.isocalendar() if d.month != self.curmonth: self.curmonth = d.month self.fridays = 0 # 周一=1 ... 周日=7 if isowkday == 5 and self.curmonth in [3, 6, 9, 12]: self.fridays += 1 if self.fridays == 3: # 第三个星期五 return True # 定时器允许 return False # 定时器不允许 然后将 allow=FutOpExp() 传递给定时器的创建。\nadd_timer 的参数 # when：可以是 datetime.time 实例（配合 tzdata 使用） bt.timer.SESSION_START 引用会话开始时间 bt.timer.SESSION_END 引用会话结束时间 offset：datetime.timedelta 实例，偏移 when 的值。与 SESSION_START/SESSION_END 结合使用，表示定时器在会话开始后 15 分钟调用。 repeat：datetime.timedelta 实例，首次调用后在同一会话内的重复间隔。 weekdays：排序的整数列表，指定定时器在星期几生效（ISO 代码，周一为 1，周日为 7）。未指定时，所有天均有效。 weekcarry（默认：False）：为 True 时，如果指定日期未出现（如假日），定时器会在下一天执行（可能是新的一周）。 monthdays：排序的整数列表，指定定时器在每月哪些天生效。未指定时，所有天均有效。 monthcarry（默认：True）：如果指定日期未出现（周末或假日），定时器在下一个可用日执行。 allow（默认：None）：回调函数，接收 datetime.date 实例，返回 True 表示该日期允许定时器触发。 tzdata：可为 None、pytz 实例或数据源实例。 None：when 按原样解释（相当于视为 UTC）。 pytz 实例：when 在指定时区的本地时间解释。 数据源实例：when 在数据源实例的 tz 参数指定的时区解释。 strats（默认：False）：是否调用策略的 notify_timer。 cheat（默认：False）：为 True 时，定时器在经纪人评估订单前调用。可在会话开始前根据开盘价下单。 *args：额外参数，传递给 notify_timer。 **kwargs：额外关键字参数，传递给 notify_timer。 示例用法 scheduled.py # $ ./scheduled.py --help 输出结果：\nusage: scheduled.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] Sample Skeleton optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) 示例源代码 scheduled.py # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class St(bt.Strategy): params = dict( when=bt.timer.SESSION_START, timer=True, cheat=False, offset=datetime.timedelta(), repeat=datetime.timedelta(), weekdays=[], ) def __init__(self): bt.ind.SMA() if self.p.timer: self.add_timer( when=self.p.when, offset=self.p.offset, repeat=self.p.repeat, weekdays=self.p.weekdays, ) if self.p.cheat: self.add_timer( when=self.p.when, offset=self.p.offset, repeat=self.p.repeat, cheat=True, ) self.order = None def prenext(self): self.next() def next(self): _, isowk, isowkday = self.datetime.date().isocalendar() txt = \u0026#39;{}, {}, Week {}, Day {}, O {}, H {}, L {}, C {}\u0026#39;.format( len(self), self.datetime.datetime(), isowk, isowkday, self.data.open[0], self.data.high[0], self.data.low[0], self.data.close[0]) print(txt) def notify_timer(self, timer, when, *args, **kwargs): print(\u0026#39;strategy notify_timer with tid {}, when {} cheat {}\u0026#39;. format(timer.p.tid, when, timer.p.cheat)) if self.order is None and timer.p.cheat: print(\u0026#39;-- {} Create buy order\u0026#39;.format(self.data.datetime.date())) self.order = self.buy() def notify_order(self, order): if order.status == order.Completed: print(\u0026#39;-- {} Buy Exec @ {}\u0026#39;.format( self.data.datetime.date(), order.executed.price)) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # 数据源 kwargs kwargs = dict( timeframe=bt.TimeFrame.Days, compression=1, sessionstart=datetime.time(9, 0), sessionend=datetime.time(17, 30), ) # 解析 from/to-date dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # 数据源 data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) # 经纪人 cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # 调整器 cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # 策略 cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # 执行 cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # 如果请求则绘图 cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;Sample Skeleton\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) # 日期默认值 parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() 示例源代码 scheduled-min.py # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class St(bt.Strategy): params = dict( when=bt.timer.SESSION_START, timer=True, cheat=False, offset=datetime.timedelta(), repeat=datetime.timedelta(), weekdays=[], weekcarry=False, monthdays=[], monthcarry=True, ) def __init__(self): bt.ind.SMA() if self.p.timer: self.add_timer( when=self.p.when, offset=self.p.offset, repeat=self.p.repeat, weekdays=self.p.weekdays, weekcarry=self.p.weekcarry, monthdays=self.p.monthdays, monthcarry=self.p.monthcarry, # tzdata=self.data0, ) if self.p.cheat: self.add_timer( when=self.p.when, offset=self.p.offset, repeat=self.p.repeat, weekdays=self.p.weekdays, weekcarry=self.p.weekcarry, monthdays=self.p.monthdays, monthcarry=self.p.monthcarry, # tzdata=self.data0, cheat=True, ) self.order = None def prenext(self): self.next() def next(self): _, isowk, isowkday = self.datetime.date().isocalendar() txt = \u0026#39;{}, {}, Week {}, Day {}, O {}, H {}, L {}, C {}\u0026#39;.format( len(self), self.datetime.datetime(), isowk, isowkday, self.data.open[0], self.data.high[0], self.data.low[0], self.data.close[0]) print(txt) def notify_timer(self, timer, when, *args, **kwargs): print(\u0026#39;strategy notify_timer with tid {}, when {} cheat {}\u0026#39;. format(timer.p.tid, when, timer.p.cheat)) if self.order is None and timer.params.cheat: print(\u0026#39;-- {} Create buy order\u0026#39;.format( self.data.datetime.datetime())) self.order = self.buy() def notify_order(self, order): if order.status == order.Completed: print(\u0026#39;-- {} Buy Exec @ {}\u0026#39;.format( self.data.datetime.datetime(), order.executed.price)) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # 数据源 kwargs kwargs = dict( timeframe=bt.TimeFrame.Minutes, compression=5, sessionstart=datetime.time(9, 0), sessionend=datetime.time(17, 30), ) # 解析 from/to-date dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # 数据源 data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) # 经纪人 cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # 调整器 cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # 策略 cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # 执行 cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # 如果请求则绘图 cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;Timer Test Intraday\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;../../datas/2006-min-005.txt\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) # 日期默认值 parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/17-datetime/02-timer/","section":"教程","summary":"在版本 1.9.44.116 中，backtrader 添加了定时器功能，可以在特定时间点调用 notify_timer（在 Cerebro 和 Strategy 中可用），支持精细控制。\n选项 # 基于绝对时间或相对于会话开始/结束时间的定时器。 时区指定，可通过 pytz 兼容对象或数据源会话时间指定。 相对于指定时间的起始偏移量。 重复间隔。 工作日过滤器（支持结转）。 月份天数过滤器（支持结转）。 自定义回调过滤器。 使用模式 # 在 Cerebro 和 Strategy 子类中，定时器回调通过以下方法接收：\n","title":"定时器与周期任务","type":"docs"},{"content":"回测无法完全模拟真实市场条件。无论模拟多好，真实市场中滑点仍可能发生，即请求的价格可能无法匹配。\n集成的回测 Broker 支持滑点，可通过以下参数配置：\n参数名 默认值 描述 slip_perc 0.0 应用于买卖订单的价格上下滑动的绝对百分比（且为正值），注意：0.01 是 1%，0.001 是 0.1%； slip_fixed 0.0 应用于买卖订单的价格上下滑动的单位百分比（且为正值），注意：如果 slip_perc 非零，则优先于此。 slip_open False 是否为专门使用下一个柱的开盘价执行的订单滑动价格。例如，市场订单将在下一个可用tick执行，即柱的开盘价。这也适用于其他一些执行，因为逻辑尝试检测开盘价是否会匹配请求的价格/执行类型在移动到新柱时。 slip_match True - 如果为 True，经纪商将通过在高/低价位封顶滑点来提供匹配，以防它们超出。- 如果为 False，经纪商将不会使用当前价格匹配订单，并将在下一次迭代中尝试执行 slip_limit True - 限价订单，给定确切的匹配价格请求，即使 slip_match 为 False，也会被匹配。- 此选项控制该行为。- 如果为 True，那么限价订单将通过在限价/高低价位封顶价格进行匹配。- 如果为 False 且滑点超出上限，则不会有匹配 slip_out False 即使价格超出高-低范围，也提供滑点。 工作原理 # 滑点应用逻辑取决于订单执行类型：\nClose - 不应用滑点\n订单匹配收盘价（当天最后一个价格），滑点无法发生，因为这是会话的最后一个价格，没有浮动空间。 Market - 应用滑点\n注意 slip_open 的例外情况。市场订单匹配下一根柱的开盘价。 Limit - 按以下逻辑应用滑点\n如果匹配价格是开盘价，根据 slip_open 参数决定是否应用滑点（如果应用，价格不会比限价更差）。 如果匹配价格不是限价，滑点在高/低点封顶，slip_limit 决定超过封顶时是否匹配。 如果匹配价格就是限价，不应用滑点。 Stop - 触发后，应用与 Market 相同的逻辑\nStopLimit - 触发后，应用与 Limit 相同的逻辑\n这种方法在回测和可用数据的限制范围内，提供最符合实际的模拟。\n配置滑点 # 每次运行时，Cerebro 引擎已使用默认参数实例化一个 Broker。有两种方式配置滑点。\n配置滑点为基于百分比的方法\nBackBroker.set_slippage_perc( perc, slip_open=True, slip_limit=True, slip_match=True, slip_out=False, ) 配置滑点为固定点数的方法：\nBackBroker.set_slippage_fixed( fixed, slip_open=True, slip_limit=True, slip_match=True, slip_out=False, ) 替换经纪商，例如：\nimport backtrader as bt cerebro = bt.Cerebro() cerebro.broker = bt.brokers.BackBroker(slip_perc=0.005) # 0.5% 实际示例 # 源码中包含一个使用 Market 订单和信号的多空示例，可帮助理解逻辑。\n先运行无滑点的版本作为参考基准：\n$ ./slippage.py --plot 01 2005-03-22 23:59:59 SELL Size: -1 / Price: 3040.55 02 2005-04-11 23:59:59 BUY Size: +1 / Price: 3088.47 03 2005-04-11 23:59:59 BUY Size: +1 / Price: 3088.47 04 2005-04-19 23:59:59 SELL Size: -1 / Price: 2948.38 05 2005-04-19 23:59:59 SELL Size: -1 / Price: 2948.38 06 2005-05-19 23:59:59 BUY Size: +1 / Price: 3034.88 ... 35 2006-12-19 23:59:59 BUY Size: +1 / Price: 4121.01 使用配置为 1.5% 滑点的相同运行：\n$ ./slippage.py --slip_perc 0.015 01 2005-03-22 23:59:59 SELL Size: -1 / Price: 3040.55 02 2005-04-11 23:59:59 BUY Size: +1 / Price: 3088.47 03 2005-04-11 23:59:59 BUY Size: +1 / Price: 3088.47 04 2005-04-19 23:59:59 SELL Size: -1 / Price: 2948.38 05 2005-04-19 23:59:59 SELL Size: -1 / Price: 2948.38 06 2005-05-19 23:59:59 BUY Size: +1 / Price: 3034.88 ... 35 2006-12-19 23:59:59 BUY Size: +1 / Price: 4121.01 没有变化。这是预期行为。\n因为执行类型为 Market，且未设置 slip_open=True。\n市场订单匹配下一个柱的开盘价，但未允许开盘价滑动。\n设置 slip_open=True 后运行：\n$ ./slippage.py --slip_perc 0.015 --slip_open 01 2005-03-22 23:59:59 SELL Size: -1 / Price: 3021.66 02 2005-04-11 23:59:59 BUY Size: +1 / Price: 3088.47 03 2005-04-11 23:59:59 BUY Size: +1 / Price: 3088.47 04 2005-04-19 23:59:59 SELL Size: -1 / Price: 2948.38 05 2005-04-19 23:59:59 SELL Size: -1 / Price: 2948.38 06 2005-05-19 23:59:59 BUY Size: +1 / Price: 3055.14 ... 35 2006-12-19 23:59:59 BUY Size: +1 / Price: 4121.01 可以看到价格已经发生变化。分配的价格更差或相同（如操作 35）。2016-12-19 的开盘价和最高价相同，价格不能高于最高价，否则会返回不存在的数据。\n当然，如果需要，Backtrader 也允许在高-低范围外匹配。启用 slip_out 后运行：\n$ ./slippage.py --slip_perc 0.015 --slip_open --slip_out 01 2005-03-22 23:59:59 SELL Size: -1 / Price: 2994.94 02 2005-04-11 23:59:59 BUY Size: +1 / Price: 3134.80 03 2005-04-11 23:59:59 BUY Size: +1 / Price: 3134.80 04 2005-04-19 23:59:59 SELL Size: -1 / Price: 2904.15 05 2005-04-19 23:59:59 SELL Size: -1 / Price: 2904.15 06 2005-05-19 23:59:59 BUY Size: +1 / Price: 3080.40 ... 35 2006-12-19 23:59:59 BUY Size: +1 / Price: 4182.83 匹配价格令人震惊，明显超出范围。以操作 35 为例，匹配价格 4182.83，而图表显示资产价格从未接近该值。\nslip_match 默认值为 True，Backtrader 会在价格封顶或未封顶时都提供匹配。禁用它：\n$ ./slippage.py --slip_perc 0.015 --slip_open --no-slip_match 01 2005-04-15 23:59:59 SELL Size: -1 / Price: 3028.10 02 2005-05-18 23:59:59 BUY Size: +1 / Price: 3029.40 03 2005-06-01 23:59:59 BUY Size: +1 / Price: 3124.03 04 2005-10-06 23:59:59 SELL Size: -1 / Price: 3365.57 05 2005-10-06 23:59:59 SELL Size: -1 / Price: 3365.57 06 2005-12-01 23:59:59 BUY Size: +1 / Price: 3499.95 07 2005-12-01 23:59:59 BUY Size: +1 / Price: 3499.95 08 2006-02-28 23:59:59 SELL Size: -1 / Price: 3782.71 09 2006-02-28 23:59:59 SELL Size: -1 / Price: 3782.71 10 2006-05-23 23:59:59 BUY Size: +1 / Price: 3594.68 11 2006-05-23 23:59:59 BUY Size: +1 / Price: 3594.68 12 2006-11-27 23:59:59 SELL Size: -1 / Price: 3984.37 13 2006-11-27 23:59:59 SELL Size: -1 / Price: 3984.37 操作从 35 次降至 13 次。原理：\n禁用 slip_match 后，当滑点将价格推高至最高价以上或最低价以下时，不允许匹配。1.5% 的滑点导致约 22 个操作无法执行。\n这些例子展示了不同滑点选项如何协同工作。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/10-broker/02-slippage/","section":"教程","summary":"回测无法完全模拟真实市场条件。无论模拟多好，真实市场中滑点仍可能发生，即请求的价格可能无法匹配。\n集成的回测 Broker 支持滑点，可通过以下参数配置：\n参数名 默认值 描述 slip_perc 0.0 应用于买卖订单的价格上下滑动的绝对百分比（且为正值），注意：0.01 是 1%，0.001 是 0.1%； slip_fixed 0.0 应用于买卖订单的价格上下滑动的单位百分比（且为正值），注意：如果 slip_perc 非零，则优先于此。 slip_open False 是否为专门使用下一个柱的开盘价执行的订单滑动价格。例如，市场订单将在下一个可用tick执行，即柱的开盘价。这也适用于其他一些执行，因为逻辑尝试检测开盘价是否会匹配请求的价格/执行类型在移动到新柱时。 slip_match True - 如果为 True，经纪商将通过在高/低价位封顶滑点来提供匹配，以防它们超出。- 如果为 False，经纪商将不会使用当前价格匹配订单，并将在下一次迭代中尝试执行 slip_limit True - 限价订单，给定确切的匹配价格请求，即使 slip_match 为 False，也会被匹配。- 此选项控制该行为。- 如果为 True，那么限价订单将通过在限价/高低价位封顶价格进行匹配。- 如果为 False 且滑点超出上限，则不会有匹配 slip_out False 即使价格超出高-低范围，也提供滑点。 工作原理 # 滑点应用逻辑取决于订单执行类型：\n","title":"滑点模拟与参数配置","type":"docs"},{"content":"Ticket #89 是关于添加基准测试来对比资产表现。这个功能非常实用，因为有些策略即使盈利，也可能不如单纯追踪资产的收益。\nbacktrader 包含两种不同类型的对象可以帮助进行追踪：\n观察器 分析器 分析器方面，已经有一个 TimeReturn 对象，用于跟踪整个投资组合价值的回报变化（包括现金）。\n这显然也可以作为一个观察器。因此在添加基准测试时，也对观察器和分析器的组合进行了工作，两者旨在跟踪相同的内容。\n注意\n观察器和分析器之间的主要区别在于观察器的线条特性：观察器记录每个值，适合绘图和实时查询，但会消耗内存。\n另一方面，分析器通过 get_analysis 返回一组结果，可能直到运行结束时才会提供任何结果。\n分析器 - 基准测试 # 标准的 TimeReturn 分析器已扩展为支持跟踪数据源。两个主要参数：\ntimeframe（默认：None）如果为 None，报告整个回测期间的总回报。 data（默认：None）要跟踪的参考资产，而非投资组合价值。 注意\n此数据必须已通过 adddata、resampledata 或 replaydata 添加到 cerebro 实例中。\n更多详细信息和参数请参阅：分析器参考。\n因此，可以按年度跟踪投资组合的回报，示例如下：\nimport backtrader as bt cerebro = bt.Cerebro() cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years) # 添加数据、策略等... results = cerebro.run() strat0 = results[0] # 如果没有指定名称，则名称为类名的小写形式 tret_analyzer = strat0.analyzers.getbyname(\u0026#39;timereturn\u0026#39;) print(tret_analyzer.get_analysis()) 如果想跟踪某个数据的回报：\nimport backtrader as bt cerebro = bt.Cerebro() data = bt.feeds.OneOfTheFeeds(dataname=\u0026#39;abcde\u0026#39;, ...) cerebro.adddata(data) cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years, data=data) # 添加策略等... results = cerebro.run() strat0 = results[0] # 如果没有指定名称，则名称为类名的小写形式 tret_analyzer = strat0.analyzers.getbyname(\u0026#39;timereturn\u0026#39;) print(tret_analyzer.get_analysis()) 如果要同时跟踪两者，建议为分析器指定名称：\nimport backtrader as bt cerebro = bt.Cerebro() data = bt.feeds.OneOfTheFeeds(dataname=\u0026#39;abcde\u0026#39;, ...) cerebro.adddata(data) cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years, data=data, _name=\u0026#39;datareturns\u0026#39;) cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years, _name=\u0026#39;timereturns\u0026#39;) # 添加策略等... results = cerebro.run() strat0 = results[0] # 获取分析结果 tret_analyzer = strat0.analyzers.getbyname(\u0026#39;timereturns\u0026#39;) print(tret_analyzer.get_analysis()) tdata_analyzer = strat0.analyzers.getbyname(\u0026#39;datareturns\u0026#39;) print(tdata_analyzer.get_analysis()) 观察器 - 基准测试 # 得益于在观察器中使用分析器的底层机制，添加了两个新观察器：\nTimeReturn Benchmark 两者都使用 bt.analyzers.TimeReturn 分析器来收集结果。\n与上述代码片段相比，以下是一个完整的示例，以展示它们的功能。\n观察 TimeReturn # 执行：\n$ ./observer-benchmark.py --plot --timereturn --timeframe notimeframe 注意执行选项：\n--timereturn 告诉示例仅执行此操作。 --timeframe notimeframe 告诉分析器考虑整个数据集而不考虑时间框架边界。 最后绘制的值是 -0.26。\n起始现金为 50K 货币单位（从图表中可以明显看出），策略最终以 36,970 货币单位收尾，价值减少了 -26%。\n观察基准测试 # 因为基准测试还会显示 timereturn 结果，让我们启用基准测试来运行同样的操作：\n$ ./observer-benchmark.py --plot --timeframe notimeframe 结果显示：\n策略表现优于资产：-0.26 对 -0.33。\n这不值得庆祝，但至少说明策略并不比资产差。\n按年度跟踪的情况：\n$ ./observer-benchmark.py --plot --timeframe years 注意：\n策略的最后一个值略有变化，从 -0.26 变为 -0.27。\n而资产的最后一个值则显示为 -0.35（相比之前的 -0.33）。\n原因是在从 2005 年到 2006 年的过渡中，策略和基准资产在 2005 年初几乎处于起始水平。\n切换到周时间框架时，整个图像发生了变化：\n$ ./observer-benchmark.py --plot --timeframe weeks 现在：\n基准观察器显示出更加紧密的波动。事物上下波动，因为现在跟踪的是投资组合和数据的每周回报。\n由于在年底最后一周没有进行任何交易，且资产几乎没有变化，最后显示的值为 0.00（最后一周之前的最后收盘价为 25.54，样本数据收盘于 25.55，差异体现在小数点后第四位）。\n观察基准测试 - 另一个数据 # 示例允许对比不同的数据。默认情况下使用 --benchdata1 基准对比 Oracle。考虑整个数据集时使用 --timeframe notimeframe：\n$ ./observer-benchmark.py --plot --timeframe notimeframe --benchdata1 结果显示：\n策略的结果未随 notimeframe 变化，仍为 -26%（-0.26）。\n但与此对比的数据在同一期间内有 +23%（0.23）的增长。\n要么需要更改策略，要么需要选择更好的资产进行交易。\n结论 # 现在有两种方式，使用相同的底层代码/计算来跟踪 TimeReturn 和 Benchmark：\n观察器（TimeReturn 和 Benchmark） 分析器（TimeReturn 和带 data 参数的 TimeReturn） 当然，基准测试并不保证利润，只是提供比较。\n示例代码 # $ ./observer-benchmark.py --help usage: observer-benchmark.py [-h] [--data0 DATA0] [--data1 DATA1] [--benchdata1] [--fromdate FROMDATE] [--todate TODATE] [--printout] [--cash CASH] [--period PERIOD] [--stake STAKE] [--timereturn] [--timeframe {months,days,notimeframe,years,None,weeks}] [--plot [kwargs]] Benchmark/TimeReturn Observers Sample optional arguments: -h, --help show this help message and exit --data0 DATA0 Data0 to be read in (default: ../../datas/yhoo-1996-2015.txt) --data1 DATA1 Data1 to be read in (default: ../../datas/orcl-1995-2014.txt) --benchdata1 Benchmark against data1 (default: False) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: 2005-01-01) --todate TODATE Ending date in YYYY-MM-DD format (default: 2006-12-31) --printout Print data lines (default: False) --cash CASH Cash to start with (default: 50000) --period PERIOD Period for the crossover moving average (default: 30) --stake STAKE Stake to apply for the buy operations (default: 1000) --timereturn Use TimeReturn observer instead of Benchmark (default: None) --timeframe {months,days,notimeframe,years,None,weeks} TimeFrame to apply to the Observer (default: None) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example: --plot style=\u0026#34;candle\u0026#34; (to plot candles) (default: None) 示例代码：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import random import backtrader as bt class St(bt.Strategy): params = ( (\u0026#39;period\u0026#39;, 10), (\u0026#39;printout\u0026#39;, False), (\u0026#39;stake\u0026#39;, 1000), ) def __init__(self): sma = bt.indicators.SMA(self.data, period=self.p.period) self.crossover = bt.indicators.CrossOver(self.data, sma) def start(self): if self.p.printout: txtfields = list() txtfields.append(\u0026#39;Len\u0026#39;) txtfields.append(\u0026#39;Datetime\u0026#39;) txtfields.append(\u0026#39;Open\u0026#39;) txtfields.append(\u0026#39;High\u0026#39;) txtfields.append(\u0026#39;Low\u0026#39;) txtfields.append(\u0026#39;Close\u0026#39;) txtfields.append(\u0026#39;Volume\u0026#39;) txtfields.append(\u0026#39;OpenInterest\u0026#39;) print(\u0026#39;,\u0026#39;.join(txtfields)) def next(self): if self.p.printout: # 仅打印第一个数据... 只是检查运行情况 txtfields = list() txtfields.append(\u0026#39;%04d\u0026#39; % len(self)) txtfields.append(self.data.datetime.datetime(0).isoformat()) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.open[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.high[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.low[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.close[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.volume[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.openinterest[0]) print(\u0026#39;,\u0026#39;.join(txtfields)) if self.position: if self.crossover \u0026lt; 0.0: if self.p.printout: print(\u0026#39;CLOSE {} @%{}\u0026#39;.format(self.p.stake, self.data.close[0])) self.close() else: if self.crossover \u0026gt; 0.0: self.buy(size=self.p.stake) if self.p.printout: print(\u0026#39;BUY {} @%{}\u0026#39;.format(self.p.stake, self.data.close[0])) TIMEFRAMES = { None: None, \u0026#39;days\u0026#39;: bt.TimeFrame.Days, \u0026#39;weeks\u0026#39;: bt.TimeFrame.Weeks, \u0026#39;months\u0026#39;: bt.TimeFrame.Months, \u0026#39;years\u0026#39;: bt.TimeFrame.Years, \u0026#39;notimeframe\u0026#39;: bt.TimeFrame.NoTimeFrame, } def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() cerebro.broker.set_cash(args.cash) dkwargs = dict() if args.fromdate: fromdate = datetime.datetime.strptime(args.fromdate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;fromdate\u0026#39;] = fromdate if args.todate: todate = datetime.datetime.strptime(args.todate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;todate\u0026#39;] = todate data0 = bt.feeds.YahooFinanceCSVData(dataname=args.data0, **dkwargs) cerebro.adddata(data0, name=\u0026#39;Data0\u0026#39;) cerebro.addstrategy(St, period=args.period, stake=args.stake, printout=args.printout) if args.timereturn: cerebro.addobserver(bt.observers.TimeReturn, timeframe=TIMEFRAMES[args.timeframe]) else: benchdata = data0 if args.benchdata1: data1 = bt.feeds.YahooFinanceCSVData(dataname=args.data1, **dkwargs) cerebro.adddata(data1, name=\u0026#39;Data1\u0026#39;) benchdata = data1 cerebro.addobserver(bt.observers.Benchmark, data=benchdata, timeframe=TIMEFRAMES[args.timeframe]) cerebro.run() if args.plot: pkwargs = dict() if args.plot is not True: # 评估为 True 但不是 True pkwargs = eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;) # 传递的参数 cerebro.plot(**pkwargs) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Benchmark/TimeReturn Observers Sample\u0026#39;) parser.add_argument(\u0026#39;--data0\u0026#39;, required=False, default=\u0026#39;../../datas/yhoo-1996-2015.txt\u0026#39;, help=\u0026#39;Data0 to be read in\u0026#39;) parser.add_argument(\u0026#39;--data1\u0026#39;, required=False, default=\u0026#39;../../datas/orcl-1995-2014.txt\u0026#39;, help=\u0026#39;Data1 to be read in\u0026#39;) parser.add_argument(\u0026#39;--benchdata1\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Benchmark against data1\u0026#39;)) parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;2005-01-01\u0026#39;, help=\u0026#39;Starting date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;2006-12-31\u0026#39;, help=\u0026#39;Ending date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--printout\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Print data lines\u0026#39;)) parser.add_argument(\u0026#39;--cash\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, type=float, default=50000, help=(\u0026#39;Cash to start with\u0026#39;)) parser.add_argument(\u0026#39;--period\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, type=int, default=30, help=(\u0026#39;Period for the crossover moving average\u0026#39;)) parser.add_argument(\u0026#39;--stake\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, type=int, default=1000, help=(\u0026#39;Stake to apply for the buy operations\u0026#39;)) parser.add_argument(\u0026#39;--timereturn\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, default=None, help=(\u0026#39;Use TimeReturn observer instead of Benchmark\u0026#39;)) parser.add_argument(\u0026#39;--timeframe\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, default=None, choices=TIMEFRAMES.keys(), help=(\u0026#39;TimeFrame to apply to the Observer\u0026#39;)) parser.add_argument(\u0026#39;--plot\u0026#39;, \u0026#39;-p\u0026#39;, nargs=\u0026#39;?\u0026#39;, required=False, metavar=\u0026#39;kwargs\u0026#39;, const=True, help=(\u0026#39;Plot the read data applying any kwargs passed\\nFor example:\\n\\n --plot style=\u0026#34;candle\u0026#34; (to plot candles)\\n\u0026#39;)) if pargs: return parser.parse_args(pargs) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/13-observers/02-benchmarking/","section":"教程","summary":"Ticket #89 是关于添加基准测试来对比资产表现。这个功能非常实用，因为有些策略即使盈利，也可能不如单纯追踪资产的收益。\nbacktrader 包含两种不同类型的对象可以帮助进行追踪：\n","title":"基准测试与绩效对比","type":"docs"},{"content":"在 Backtrader 社区中，经常有用户希望复制 TradingView 等平台上的回测结果。TradingView 使用 Pinescript，用户往往不了解其实现细节和回测引擎的内部机制。因此，即使有意复制，也必须明白跨平台回测有其局限性。\n指标：并不总是忠实于原始定义 # 在 Backtrader 中实现新指标时，开发者注重尊重原始定义。RSI 就是典型例子。\nWelles Wilder 设计 RSI 时使用了修改过的移动平均（即平滑移动平均，参见 Wikipedia - Modified Moving Average）。但许多平台提供的 RSI 实际上使用的是经典指数移动平均（EMA），而非原始定义。\n虽然两者差异不大，但这并非 Wilder 原始定义的 RSI。它可能仍然有用甚至更好，但并非 Wilder 所定义的 RSI。而且，大多数文档并未提及这一点。\n在 Backtrader 中，RSI 默认使用 MMA 以保持忠实于原始定义，但可以通过子类化或运行时实例化选择使用 EMA 或 SMA。\n例子：唐奇安通道 # Wikipedia 给出了定义：Wikipedia - Donchian Channel，但仅为文字描述，未说明如何使用通道突破作为交易信号。\n另外，以下来源明确说明计算通道时不包含当前 K 线，否则突破无法被体现：\nStockCharts - School - Price Channels IncredibleCharts - Donchian Channels 这些来源明确指出，计算通道时不包含当前价格 K 线，这样突破才能正确显示。来自 StockCharts 的示例图表：\nStockCharts - Donchian Channels - Breakouts\n再看看 TradingView。首先是链接：TradingView - Wiki - Donchian Channels\n以及页面图表：\nTradingView - Donchian Channels - No Breakouts\n甚至 Investopedia 也使用了 TradingView 的图表，显示没有突破：\nInvestopedia - Donchian Channels\n许多人可能会惊讶，TradingView 的 Donchian 通道没有显示突破。这意味着 TradingView 的实现将当前价格 K 线也纳入了计算。\nBacktrader 中的唐奇安通道 # Backtrader 没有内置的 DonchianChannels，但可以轻松创建。是否将当前 K 线纳入通道计算，是可以调节的参数。\n以下是代码示例：\nclass DonchianChannels(bt.Indicator): \u0026#39;\u0026#39;\u0026#39; 参数说明： - ``lookback``（默认：-1） 如果是 `-1`，则考虑从过去一根柱线开始计算，当前的高/低价可能突破通道。 如果是 `0`，则当前的价格将被用于计算 Donchian 通道。这意味着价格**永远**不会突破上下通道带。 \u0026#39;\u0026#39;\u0026#39; alias = (\u0026#39;DCH\u0026#39;, \u0026#39;DonchianChannel\u0026#39;,) lines = (\u0026#39;dcm\u0026#39;, \u0026#39;dch\u0026#39;, \u0026#39;dcl\u0026#39;,) # dc 中线，dc 高线，dc 低线 params = dict( period=20, lookback=-1, # 是否考虑当前柱线 ) plotinfo = dict(subplot=False) # 与数据一起绘制 plotlines = dict( dcm=dict(ls=\u0026#39;--\u0026#39;), # 虚线 dch=dict(_samecolor=True), # 使用与前一个线条相同的颜色（dcm） dcl=dict(_samecolor=True), # 使用与前一个线条相同的颜色（dch） ) def __init__(self): hi, lo = self.data.high, self.data.low if self.p.lookback: # 根据需要向后移动 hi, lo = hi(self.p.lookback), lo(self.p.lookback) self.l.dch = bt.ind.Highest(hi, period=self.p.period) self.l.dcl = bt.ind.Lowest(lo, period=self.p.period) self.l.dcm = (self.l.dch + self.l.dcl) / 2.0 # 上下通道的平均值 使用 lookback=-1 的配置，生成的图表如下（放大查看）：\nBacktrader - Donchian Channels - Breakouts\n可以清晰地看到突破，而使用 lookback=0 时则看不到突破。\nBacktrader - Donchian Channels - No Breakouts\n编码中的隐含问题 # 如果程序员先在商业平台上实现策略，且由于图表未显示突破，可能会写出如下代码：\nif price0 \u0026gt; channel_high_1: sell() elif price0 \u0026lt; channel_low_1: buy() 其中，price0 会与前一周期的通道高/低进行比较（因此 _1 后缀表示前一周期）。\n然而，在不了解 Backtrader 中 Donchian 通道默认包含突破的情况下，程序员会编写如下代码：\ndef __init__(self): self.donchian = DonchianChannels() def next(self): if self.data[0] \u0026gt; self.donchian.dch[-1]: self.sell() elif self.data[0] \u0026lt; self.donchian.dcl[-1]: self.buy() 这是错误的！因为突破在比较发生时已经体现。正确的代码是：\ndef __init__(self): self.donchian = DonchianChannels() def next(self): if self.data[0] \u0026gt; self.donchian.dch[0]: self.sell() elif self.data[0] \u0026lt; self.donchian.dcl[0]: self.buy() 虽然这只是一个小例子，但它揭示了回测结果如何因指标实现的差异而产生不同的情况。虽然差别看起来不大，但错误的交易判断可能会带来巨大的影响。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/19-articles/02-cross-backtesting-pitfalls/","section":"教程","summary":"在 Backtrader 社区中，经常有用户希望复制 TradingView 等平台上的回测结果。TradingView 使用 Pinescript，用户往往不了解其实现细节和回测引擎的内部机制。因此，即使有意复制，也必须明白跨平台回测有其局限性。\n","title":"跨平台回测的陷阱与解决方案","type":"docs"},{"content":"使用 backtrader 不一定要编写策略类。虽然这是推荐方式，但由于对象层级结构，使用信号也是一种可行方案。\n快速总结： # 不需要编写策略类、实例化指标、编写买卖逻辑等。 添加信号（信号本质上也是指标），其余部分由后台自动处理。 快速示例： # import backtrader as bt data = bt.feeds.OneOfTheFeeds(dataname=\u0026#39;mydataname\u0026#39;) cerebro.adddata(data) cerebro.add_signal(bt.SIGNAL_LONGSHORT, MySignal) cerebro.run() 这就完成了。信号本身还没有定义。\n让我们定义一个非常简单的信号：\n如果收盘价高于简单移动平均线 (SMA)，则发出多头信号。 如果收盘价低于 SMA，则发出空头信号。 定义如下：\nclass MySignal(bt.Indicator): lines = (\u0026#39;signal\u0026#39;,) params = ((\u0026#39;period\u0026#39;, 30),) def __init__(self): self.lines.signal = self.data - bt.indicators.SMA(period=self.p.period) 现在真的完成了。\n运行 run 时，Cerebro 会实例化一个特殊的策略实例来处理这些信号。\n常见问题 # 买卖操作的数量是如何确定的？\nCerebro 自动为策略添加一个固定大小 (FixedSize) 的定量器。用户可通过 cerebro.addsizer 更改定量器。\n订单是如何执行的？\n执行类型为市价单，订单的有效期为“直到取消” (Good Until Canceled)。\n信号细节 # 从技术和理论角度来看，可以描述为：\n一个可调用对象，被调用时返回另一个对象（只调用一次）。 通常是类的实例化，但不一定。 支持 __getitem__ 接口。唯一请求的键/索引将是 0。 从实际角度来看，信号是：\n来自 backtrader 生态系统的 lines 对象，主要是指标。 这在使用其他指标时很有帮助，比如示例中的简单移动平均线。\n信号指示 # 信号在使用 signal[0] 查询时提供指示，含义如下：\n\u0026gt; 0 -\u0026gt; 多头指示 \u0026lt; 0 -\u0026gt; 空头指示 == 0 -\u0026gt; 无指示 示例中，简单地用 self.data - SMA 进行算术运算：\n当数据高于 SMA 时发出多头指示。 当数据低于 SMA 时发出空头指示。 未指定价格字段时，默认参考价格为收盘价。\n信号类型 # 如下示例中的常量，直接从主 backtrader 模块获取：\nimport backtrader as bt bt.SIGNAL_LONG 有 5 种类型的信号，分为 2 组。\n主要组 # LONGSHORT：接受来自该信号的多头和空头指示。\nLONG：\n接受多头指示进行做多。\n接受空头指示平仓多头。但：\n如果系统中有 LONGEXIT 信号，将用它来平仓多头。 如果有 SHORT 信号且没有 LONGEXIT 信号，它将被用来平仓多头再开空头。 SHORT：\n接受空头指示进行做空。 接受多头指示平仓空头。但：\n如果系统中有 SHORTEXIT 信号，将用它来平仓空头。 如果有 LONG 信号且没有 SHORTEXIT 信号，它将被用来平仓空头再开多头。 退出组 # 这两个信号旨在覆盖其他信号，并为平仓提供标准。\nLONGEXIT：接受空头指示平仓多头。 SHORTEXIT：接受多头指示平仓空头。 累积和订单并发 # 上面的示例信号会持续发出多头和空头指示，因为它只是用收盘价减去 SMA 值，结果永远 \u0026gt; 0 或 \u0026lt; 0（0 虽在数学上可能，但实际几乎不会发生）。\n这将导致连续生成订单，从而产生两种情况：\n累积：即使已有持仓，信号仍会产生新订单，增加仓位。 并发：在其他订单执行前生成新订单。 为了避免这种情况，默认行为是：\n不累积。 不允许并发。 如果需要其中任何一种行为，可以通过 Cerebro 控制：\ncerebro.signal_accumulate(True) # 或 False 禁用 cerebro.signal_concurrency(True) # 或 False 禁用 示例 # backtrader 源码中包含一个测试该功能的示例。\n主要信号如下：\nclass SMACloseSignal(bt.Indicator): lines = (\u0026#39;signal\u0026#39;,) params = ((\u0026#39;period\u0026#39;, 30),) def __init__(self): self.lines.signal = self.data - bt.indicators.SMA(period=self.p.period) 如果指定了退出信号，代码如下：\nclass SMAExitSignal(bt.Indicator): lines = (\u0026#39;signal\u0026#39;,) params = ((\u0026#39;p1\u0026#39;, 5), (\u0026#39;p2\u0026#39;, 30),) def __init__(self): sma1 = bt.indicators.SMA(period=self.p.p1) sma2 = bt.indicators.SMA(period=self.p.p2) self.lines.signal = sma1 - sma2 第一次运行：多头和空头\n$ ./signals-strategy.py --plot --signal longshort 输出：\n信号被绘制。因为它只是一个指标，所以适用绘图规则。 策略确实做多和做空。因为现金水平从未回到价值水平。 第二次运行：仅多头\n$ ./signals-strategy.py --plot --signal longonly 输出：\n现金水平在每次卖出后回到价值水平，说明策略在市场之外。 第三次运行：仅空头\n$ ./signals-strategy.py --plot --signal shortonly 输出：\n第一次操作是卖出，发生在收盘价低于 SMA 且简单减法得到负值之后。 现金水平在每次买入后回到价值水平，说明策略在市场之外。 第四次运行：多头 + 多头退出\n$ ./signals-strategy.py --plot --signal longonly --exitsignal longexit 输出：\n许多交易是相同的，但一些较早被中断，因为退出信号中的快速移动平均线向下穿过慢速移动平均线。 系统显示其仅做多特性，每笔交易结束时现金成为价值。 使用方法 # $ ./signals-strategy.py --help 完整示例 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import collections import datetime import backtrader as bt MAINSIGNALS = collections.OrderedDict( ((\u0026#39;longshort\u0026#39;, bt.SIGNAL_LONGSHORT), (\u0026#39;longonly\u0026#39;, bt.SIGNAL_LONG), (\u0026#39;shortonly\u0026#39;, bt.SIGNAL_SHORT),) ) EXITSIGNALS = { \u0026#39;longexit\u0026#39;: bt.SIGNAL_LONGEXIT, \u0026#39;shortexit\u0026#39;: bt.SIGNAL_LONGEXIT, } class SMACloseSignal(bt.Indicator): lines = (\u0026#39;signal\u0026#39;,) params = ((\u0026#39;period\u0026#39;, 30),) def __init__(self): self.lines.signal = self.data - bt.indicators.SMA(period=self.p.period) class SMAExitSignal(bt.Indicator): lines = (\u0026#39;signal\u0026#39;,) params = ((\u0026#39;p1\u0026#39;, 5), (\u0026#39;p2\u0026#39;, 30),) def __init__(self): sma1 = bt.indicators.SMA(period=self.p.p1) sma2 = bt.indicators.SMA(period=self.p.p2) self.lines.signal = sma1 - sma2 def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() cerebro.broker.set_cash(args.cash) dkwargs = dict() if args.fromdate is not None: fromdate = datetime.datetime.strptime(args.fromdate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;fromdate\u0026#39;] = fromdate if args.todate is not None: todate = datetime.datetime.strptime(args.todate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;todate\u0026#39;] = todate data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs) cerebro.adddata(data) cerebro.add_signal(MAINSIGNALS[args.signal], SMACloseSignal, period=args.smaperiod) if args.ex itsignal is not None: cerebro.add_signal(EXITSIGNALS[args.exitsignal], SMAExitSignal, p1=args.exitperiod, p2=args.smaperiod) cerebro.run() if args.plot: pkwargs = dict(style=\u0026#39;bar\u0026#39;) if args.plot is not True: npkwargs = eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;) pkwargs.update(npkwargs) cerebro.plot(**pkwargs) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Sample for Signal concepts\u0026#39;) parser.add_argument(\u0026#39;--data\u0026#39;, required=False, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, help=\u0026#39;Specific data to be read in\u0026#39;) parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=None, help=\u0026#39;Starting date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=None, help=\u0026#39;Ending date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--cash\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, type=float, default=50000, help=(\u0026#39;Cash to start with\u0026#39;)) parser.add_argument(\u0026#39;--smaperiod\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, type=int, default=30, help=(\u0026#39;Period for the moving average\u0026#39;)) parser.add_argument(\u0026#39;--exitperiod\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, type=int, default=5, help=(\u0026#39;Period for the exit control SMA\u0026#39;)) parser.add_argument(\u0026#39;--signal\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, default=MAINSIGNALS.keys()[0], choices=MAINSIGNALS, help=(\u0026#39;Signal type to use for the main signal\u0026#39;)) parser.add_argument(\u0026#39;--exitsignal\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, default=None, choices=EXITSIGNALS, help=(\u0026#39;Signal type to use for the exit signal\u0026#39;)) parser.add_argument(\u0026#39;--plot\u0026#39;, \u0026#39;-p\u0026#39;, nargs=\u0026#39;?\u0026#39;, required=False, metavar=\u0026#39;kwargs\u0026#39;, const=True, help=(\u0026#39;Plot the read data applying any kwargs passed\\n\u0026#39; \u0026#39;\\n\u0026#39; \u0026#39;For example:\\n\u0026#39; \u0026#39;\\n\u0026#39; \u0026#39; --plot style=\u0026#34;candle\u0026#34; (to plot candles)\\n\u0026#39;)) if pargs is not None: return parser.parse_args(pargs) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/07-strategy/02-strategy-with-signals/","section":"教程","summary":"使用 backtrader 不一定要编写策略类。虽然这是推荐方式，但由于对象层级结构，使用信号也是一种可行方案。\n快速总结： # 不需要编写策略类、实例化指标、编写买卖逻辑等。 添加信号（信号本质上也是指标），其余部分由后台自动处理。 快速示例： # import backtrader as bt data = bt.feeds.OneOfTheFeeds(dataname='mydataname') cerebro.adddata(data) cerebro.add_signal(bt.SIGNAL_LONGSHORT, MySignal) cerebro.run() 这就完成了。信号本身还没有定义。\n","title":"信号驱动的策略模式","type":"docs"},{"content":"在 1.9.31.x 版本中，backtrader 增加了部分绘图功能。\n可以使用策略实例中保存的时间戳数组索引来指定绘图范围，\n也可以使用 datetime.date 或 datetime.datetime 实例来限制范围。\n仍通过标准 cerebro.plot 调用。例如：\ncerebro.plot(start=datetime.date(2005, 7, 1), end=datetime.date(2006, 1, 31)) 这对人类来说最直观。也可以使用时间戳的索引：\ncerebro.plot(start=75, end=185) 以下示例包含简单移动平均线（在数据上绘图）、随机指标（独立绘图）及其交叉信号，通过命令行参数传递给 cerebro.plot。\n使用日期方法的执行：\n./partial-plot.py --plot \u0026#39;start=datetime.date(2005, 7, 1),end=datetime.date(2006, 1, 31)\u0026#39; Python 的 eval 机制允许在命令行中直接编写 datetime.date 并映射为实际意义。输出图表如下：\n让我们将其与完整的绘图进行比较，以查看数据是否确实从两端跳过：\n./partial-plot.py --plot Python 的 eval 机制允许在命令行中直接编写 datetime.date 并映射为实际意义。输出图表如下：\n示例用法 # $ ./partial-plot.py --help usage: partial-plot.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] Sample for partial plotting optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) 示例代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class St(bt.Strategy): params = () def __init__(self): bt.ind.SMA() stoc = bt.ind.Stochastic() bt.ind.CrossOver(stoc.lines.percK, stoc.lines.percD) def next(self): pass def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # 数据馈送 kwargs kwargs = dict() # 解析 from/to-date dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # 数据馈送 data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) # 经纪人 cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # 大小调整器 cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # 策略 cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # 执行 cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # 如果请求绘图，则绘制 cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;Sample for partial plotting\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) # 日期的默认值 parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/16-plotting/02-ploting-date-ranges/","section":"教程","summary":"在 1.9.31.x 版本中，backtrader 增加了部分绘图功能。\n可以使用策略实例中保存的时间戳数组索引来指定绘图范围，\n也可以使用 datetime.date 或 datetime.datetime 实例来限制范围。\n仍通过标准 cerebro.plot 调用。例如：\n","title":"指定日期范围的绘图","type":"docs"},{"content":"如果要开发任何东西（除了交易策略），那就是自定义指标。在平台中开发它很容易。\n开发要点：\n从 Indicator 类（直接或从现有子类）派生一个类； 定义它将包含的 Line； 一个指标至少要有一条线。如果从现有的类派生，线条可能已经定义好了 可选地定义可以改变行为的参数 可选地提供/自定义一些用于合理绘制指标的元素 在 __init__ 中提供一个完全定义的操作，并绑定（分配）到指标的线条，或者提供 next 方法和（可选的）once 方法 如果一个指标可以在初始化时通过逻辑或算术操作完全定义，并将结果分配给线条，那就完成了。如果做不到，至少要提供 next 方法，在索引 0 处给线条赋值。还可以提供 once 方法来优化批处理模式的计算。\n重要说明：幂等性 # 指标为每个收到的条生成一个输出。同一个条可能被多次发送，因此操作必须是幂等的。\n其背后的理由：\n同一个条（同一索引）可以多次发送，且值会变化（如收盘价更新） 这使得可以“重放”一个日内会话，但使用由 5 分钟条组成的日内数据。 这也使平台能从实时数据源获取值。\n一个简单（但功能齐全）的指标 # 可以这样：\nclass DummyInd(bt.Indicator): lines = (\u0026#39;dummyline\u0026#39;,) params = ((\u0026#39;value\u0026#39;, 5),) def __init__(self): self.lines.dummyline = bt.Max(0.0, self.params.value) 完成！\n该指标始终输出相同的值：0.0 或 self.params.value（如果大于 0.0）。\n使用 next 方法的相同指标：\nclass DummyInd(bt.Indicator): lines = (\u0026#39;dummyline\u0026#39;,) params = ((\u0026#39;value\u0026#39;, 5),) def next(self): self.lines.dummyline[0] = max(0.0, self.params.value) 完成！相同行为。\n在 __init__ 版本中，使用 bt.Max 将值分配给线条对象 self.lines.dummyline。bt.Max 返回一个线条对象，它会为每个传入的条自动迭代。如果使用 max，赋值将无意义，因为指标得到的是固定值的成员变量，而非线条。在 next 中处理浮点值时，可使用标准的 max 函数。\n回顾一下，self.lines.dummyline 是长表示法，可以缩短为：\nself.l.dummyline 甚至可以缩短为：\nself.dummyline 只有在代码没有用成员属性遮蔽这个变量时，后者才有可能。\n第三个也是最后一个版本提供了一个额外的 once 方法来优化计算：\nclass DummyInd(bt.Indicator): lines = (\u0026#39;dummyline\u0026#39;,) params = ((\u0026#39;value\u0026#39;, 5),) def next(self): self.lines.dummyline[0] = max(0.0, self.params.value) def once(self, start, end): dummy_array = self.lines.dummyline.array for i in range(start, end): dummy_array[i] = max(0.0, self.params.value) 这样更高效，但开发 once 方法需要深入研究内部机制。\n__init__ 版本仍然是最好的选择：\n所有工作都在初始化时完成 自动提供 next 和 once（都已优化，因为 bt.Max 已包含它们），无需处理索引和公式 如果需要开发，指标也可以覆盖与 next 和 once 相关的方法：\nprenext 和 nextstart preonce 和 oncestart 手动/自动最小周期 # 平台会尽可能自动计算，但有时需要手动指定。\n以下是一个简单移动平均线的实现：\nclass SimpleMovingAverage1(Indicator): lines = (\u0026#39;sma\u0026#39;,) params = ((\u0026#39;period\u0026#39;, 20),) def next(self): datasum = math.fsum(self.data.get(size=self.p.period)) self.lines.sma[0] = datasum / self.p.period 虽然看似合理，但平台并不知道最小周期，即使参数名为”period”（名称可能有误导性，有些指标有多个”period”参数，用途各不相同）。\n这种情况下，next 在第一个条进入时就会被调用，导致崩溃，因为 get 无法返回所需的 self.p.period 个数据。\n解决此问题前，需要考虑：\n传递给指标的数据源可能已经携带最小周期。示例 SimpleMovingAverage 可以在以下情况下进行：\n常规数据源：默认最小周期为 1（只等待进入系统的第一个条）。 另一个移动平均线：它已有周期。如果是 20，示例移动平均线也是 20，则最终最小周期为 40 条。 实际上内部计算是 39，因为第一个移动平均线生成的条会计入下一个，产生一个重叠条，因此只需 39。其他指标/对象也带有周期。\n解决方法如下：\nclass SimpleMovingAverage1(Indicator): lines = (\u0026#39;sma\u0026#39;,) params = ((\u0026#39;period\u0026#39;, 20),) def __init__(self): self.addminperiod(self.params.period) def next(self): datasum = math.fsum(self.data.get(size=self.p.period)) self.lines.sma[0] = datasum / self.p.period addminperiod 方法告知系统该指标所需的额外周期条数，无论现有最小周期是多少。如果所有计算都使用已向系统传达周期需求的对象，则无需此操作。\n一个快速的 MACD 实现带有直方图：\nfrom backtrader.indicators import EMA class MACD(Indicator): lines = (\u0026#39;macd\u0026#39;, \u0026#39;signal\u0026#39;, \u0026#39;histo\u0026#39;,) params = ((\u0026#39;period_me1\u0026#39;, 12), (\u0026#39;period_me2\u0026#39;, 26), (\u0026#39;period_signal\u0026#39;, 9),) def __init__(self): me1 = EMA(self.data, period=self.p.period_me1) me2 = EMA(self.data, period=self.p.period_me2) self.l.macd = me1 - me2 self.l.signal = EMA(self.l.macd, period=self.p.period_signal) self.l.histo = self.l.macd - self.l.signal 完成！无需考虑最小周期。\nEMA 代表指数移动平均线（平台内置别名），已在平台中声明了它的周期需求。指标的命名线条 “macd” 和 “signal” 被分配了已在幕后声明周期的对象。\nmacd 从 me1 - me2 操作中获取周期，me1 和 me2 都是不同周期的指数移动平均线。signal 直接从 macd 上的指数移动平均线获取周期。这个 EMA 也会考虑现有的 macd 周期和自身所需的样本量（period_signal）。histo 取 “signal - macd” 两个操作数的最大值。两者都准备好后，histo 就能生成值。\n一个完整的自定义指标 # 让我们开发一个简单的自定义指标，它“指示”一个移动平均线（可以通过参数修改）是否在给定数据之上：\nimport backtrader as bt import backtrader.indicators as btind class OverUnderMovAv(bt.Indicator): lines = (\u0026#39;overunder\u0026#39;,) params = dict(period=20, movav=btind.MovAv.Simple) def __init__(self): movav = self.p.movav(self.data, period=self.p.period) self.l.overunder = bt.Cmp(movav, self.data) 完成！\n如果平均线在数据之上，指标值为 “1”；在数据之下则为 “-1”。对于常规数据源，收盘价比较会生成 1 或 -1。\n虽然绘图部分会自动处理，但为了让指标更友好易用，可以添加以下内容：\nimport backtrader as bt import backtrader.indicators as btind class OverUnderMovAv(bt.Indicator): lines = (\u0026#39;overunder\u0026#39;,) params = dict(period=20, movav=btind.MovAv.Simple) plotinfo = dict( # 在 1 和 -1 之上和之下添加额外的边距 plotymargin=0.15, # 绘制参考水平线在 1.0 和 -1.0 plothlines=[1.0, -1.0], # 简化 y 轴刻度为 1.0 和 -1.0 plotyticks=[1.0, -1.0]) # 使用虚线样式绘制 \u0026#34;overunder\u0026#34; 线（唯一的一个） # ls 代表线条样式，并直接传递给 matplotlib plotlines = dict(overunder=dict(ls=\u0026#39;--\u0026#39;)) def _plotlabel(self): # 此方法返回将在绘图上显示的标签列表 # 在指标名称之后 # 周期必须始终存在 plabels = [self.p.period] # 如果不是默认移动平均线，只放移动平均线 plabels += [self.p.movav] * self.p.notdefault(\u0026#39;movav\u0026#39;) return plabels def __init__(self): movav = self.p.movav(self.data, period=self.p.period) self.l.overunder = bt.Cmp(movav, self.data) 完成！\n这个指标将具有 1 和 -1 的值，并且在绘图中有良好的表现。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/08-indicators/02-indicators-development/","section":"教程","summary":"如果要开发任何东西（除了交易策略），那就是自定义指标。在平台中开发它很容易。\n开发要点：\n从 Indicator 类（直接或从现有子类）派生一个类； 定义它将包含的 Line； 一个指标至少要有一条线。如果从现有的类派生，线条可能已经定义好了 可选地定义可以改变行为的参数 可选地提供/自定义一些用于合理绘制指标的元素 在 __init__ 中提供一个完全定义的操作，并绑定（分配）到指标的线条，或者提供 next 方法和（可选的）once 方法 如果一个指标可以在初始化时通过逻辑或算术操作完全定义，并将结果分配给线条，那就完成了。如果做不到，至少要提供 next 方法，在索引 0 处给线条赋值。还可以提供 once 方法来优化批处理模式的计算。\n","title":"自定义技术指标开发指南","type":"docs"},{"content":"能否轻松扩展现有机制，添加额外字段并与价格数据（如开盘价、最高价等）一起传递？\n正在解析一个 CSV 格式的数据源 使用 GenericCSVData 加载数据 通用 CSV 支持是为响应 Issue #6 开发的 需要将 P/E 字段与解析的 CSV 数据一起传递 下面基于 CSV 数据源开发和 GenericCSVData 示例来演示。\n步骤： # 假设 P/E 信息已包含在 CSV 数据中 使用 GenericCSVData 作为基类 添加 pe 行扩展已有的字段（开盘价/最高价/最低价/收盘价/成交量/持仓兴趣） 添加参数，让调用者指定 P/E 列的索引 结果如下：\nfrom backtrader.feeds import GenericCSVData class GenericCSV_PE(GenericCSVData): # 添加 \u0026#39;pe\u0026#39; 行到从基类继承的行中 lines = (\u0026#39;pe\u0026#39;,) # GenericCSVData 中的 openinterest 索引为 7 ... 添加 1 # 将参数添加到从基类继承的参数中 params = ((\u0026#39;pe\u0026#39;, 8),) 这样就完成了。\n稍后在策略中使用此数据源时：\nimport backtrader as bt .... class MyStrategy(bt.Strategy): ... def next(self): if self.data.close \u0026gt; 2000 and self.data.pe \u0026lt; 12: # TORA TORA TORA --- 退出市场 self.sell(stake=1000000, price=0.01, exectype=Order.Limit) ... 绘制额外的 P/E 行 # 数据源中没有自动绘制此行数据的支持。\n替代方案是在该行上添加简单移动平均线，在单独的子图上绘制：\nimport backtrader as bt import backtrader.indicators as btind .... class MyStrategy(bt.Strategy): def __init__(self): # 指标自动注册，即使在类中没有保留明显的引用也会绘制 btind.SMA(self.data.pe, period=1, subplot=False) ... def next(self): if self.data.close \u0026gt; 2000 and self.data.pe \u0026lt; 12: # TORA TORA TORA --- 退出市场 self.sell(stake=1000000, price=0.01, exectype=Order.Limit) ... ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/02-datafeed-extending/","section":"教程","summary":"能否轻松扩展现有机制，添加额外字段并与价格数据（如开盘价、最高价等）一起传递？\n正在解析一个 CSV 格式的数据源 使用 GenericCSVData 加载数据 通用 CSV 支持是为响应 Issue #6 开发的 需要将 P/E 字段与解析的 CSV 数据一起传递 下面基于 CSV 数据源开发和 GenericCSVData 示例来演示。\n","title":"自定义扩展数据源","type":"docs"},{"content":"佣金及相关功能由一个单一的类 CommissionInfo 管理，通常通过调用 broker.setcommission 实例化。\n这个概念仅限于带有保证金和每份合约固定佣金的期货，以及基于价格/数量百分比佣金的股票。虽然不是最灵活的方案，但它已经发挥了作用。\nGitHub 上的一个增强请求（#29）导致了一些重构，以便：\n保持 CommissionInfo 和 broker.setcommission 与原始行为兼容 清理一些代码 使佣金方案更灵活，以支持增强需求和更多可能性 实际工作如下：\nclass CommInfoBase(with_metaclass(MetaParams)): COMM_PERC, COMM_FIXED = range(2) params = ( (\u0026#39;commission\u0026#39;, 0.0), (\u0026#39;mult\u0026#39;, 1.0), (\u0026#39;margin\u0026#39;, None), (\u0026#39;commtype\u0026#39;, None), (\u0026#39;stocklike\u0026#39;, False), (\u0026#39;percabs\u0026#39;, False), ) 引入了 CommissionInfo 的基类，新增了以下参数：\ncommtype（默认值：None）\n这是兼容性的关键。如果值为 None，CommissionInfo 对象和 broker.setcommission 的行为与以前相同。具体如下：\n如果设置了 margin，则佣金方案是期货，每份合约固定佣金 如果未设置 margin，则佣金方案是股票，采用百分比方式 如果值为 COMM_PERC 或 COMM_FIXED（或派生类中的任何其他值），则决定佣金是固定还是基于百分比 stocklike（默认值：False）\n如上所述，旧 CommissionInfo 对象中的实际行为由 margin 参数决定。\n如果 commtype 设置为其他值，则此参数指示资产是类似期货（使用保证金并进行基于柱状图的现金调整）还是类似股票。\npercabs（默认值：False）\n如果为 False，百分比以相对值传递（xx%）\n如果为 True，百分比以绝对值传递（0.xx）\nCommissionInfo 从 CommInfoBase 派生，将此参数的默认值改为 True 以保持兼容行为。\n所有这些参数也可以在 broker.setcommission 中使用，现在看起来像这样：\ndef setcommission(self, commission=0.0, margin=None, mult=1.0, commtype=None, percabs=True, stocklike=False, name=None): 注意：\npercabs 为 True，以保持与 CommissionInfo 旧调用的兼容性 旧的测试佣金方案示例已重写，支持命令行参数和新行为。帮助信息如下：\n$ ./commission-schemes.py --help usage: commission-schemes.py [-h] [--data DATA] [--fromdate FROMDATE] [--todate TODATE] [--stake STAKE] [--period PERIOD] [--cash CASH] [--comm COMM] [--mult MULT] [--margin MARGIN] [--commtype {none,perc,fixed}] [--stocklike] [--percrel] [--plot] [--numfigs NUMFIGS] Commission schemes optional arguments: -h, --help show this help message and exit --data DATA, -d DATA data to add to the system (default: ../../datas/2006-day-001.txt) --fromdate FROMDATE, -f FROMDATE Starting date in YYYY-MM-DD format (default: 2006-01-01) --todate TODATE, -t TODATE Starting date in YYYY-MM-DD format (default: 2006-12-31) --stake STAKE Stake to apply in each operation (default: 1) --period PERIOD Period to apply to the Simple Moving Average (default: 30) --cash CASH Starting Cash (default: 10000.0) --comm COMM Commission factor for operation, either apercentage or a per stake unit absolute value (default: 2.0) --mult MULT Multiplier for operations calculation (default: 10) --margin MARGIN Margin for futures-like operations (default: 2000.0) --commtype {none,perc,fixed} Commission - choose none for the old CommissionInfo behavior (default: none) --stocklike If the operation is for stock-like assets orfuture- like assets (default: False) --percrel If perc is expressed in relative xx% ratherthan absolute value 0.xx (default: False) --plot, -p Plot the read data (default: False) --numfigs NUMFIGS, -n NUMFIGS Plot using numfigs figures (default: 1) 让我们运行几个示例来重现原始佣金方案的行为。\n期货佣金（固定且带保证金） # 执行和图表：\n$ ./commission-schemes.py --comm 2.0 --margin 2000.0 --mult 10 --plot 输出显示固定佣金为 2.0 货币单位（默认 stake 为 1）：\n2006-03-09, BUY CREATE, 3757.59 2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00 2006-04-11, SELL CREATE, 3788.81 2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00 2006-04-12, TRADE PROFIT, GROSS 328.00, NET 324.00 ... 股票佣金（百分比且无保证金） # 执行和图表：\n$ ./commission-schemes.py --comm 0.005 --margin 0 --mult 1 --plot 为了更直观，可以使用相对百分比值：\n$ ./commission-schemes.py --percrel --comm 0.5 --margin 0 --mult 1 --plot 现在 0.5 直接表示 0.5%\n输出在两种情况下都是：\n2006-03-09, BUY CREATE, 3757.59 2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77 2006-04-11, SELL CREATE, 3788.81 2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3754.13, Comm 18.93 2006-04-12, TRADE PROFIT, GROSS 32.80, NET -4.91 ... 期货佣金（百分比且带保证金） # 使用新参数，基于百分比的期货方案：\n$ ./commission-schemes.py --commtype perc --percrel --comm 0.5 --margin 2000 --mult 10 --plot 不出所料，改变佣金后……最终结果也随之改变。\n输出显示佣金现在是可变的：\n2006-03-09, BUY CREATE, 3757.59 2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 18.77 2006-04-11, SELL CREATE, 3788.81 2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 18.93 2006-04-12, TRADE PROFIT, GROSS 328.00, NET 290.29 ... 之前的运行设置了 2.0 货币单位（默认 stake 为 1）。\n下一篇文章将详细说明新类和自定义佣金方案的实现。\n示例 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class SMACrossOver(bt.Strategy): params = ( (\u0026#39;stake\u0026#39;, 1), (\u0026#39;period\u0026#39;, 30), ) def log(self, txt, dt=None): \u0026#39;\u0026#39;\u0026#39; Logging function for this strategy\u0026#39;\u0026#39;\u0026#39; dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enougth cash if order.status in [order.Completed, order.Canceled, order.Margin]: if order.isbuy(): self.log( \u0026#39;BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) else: # Sell self.log(\u0026#39;SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) def notify_trade(self, trade): if trade.isclosed: self.log(\u0026#39;TRADE PROFIT, GROSS %.2f, NET %.2f\u0026#39; % (trade.pnl, trade.pnlcomm)) def __init__(self): sma = btind.SMA(self.data, period=self.p.period) # \u0026gt; 0 crossing up / \u0026lt; 0 crossing down self.buysell_sig = btind.CrossOver(self.data, sma) def next(self): if self.buysell_sig \u0026gt; 0: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.data.close[0]) self.buy(size=self.p.stake) # keep order ref to avoid 2nd orders elif self.position and self.buysell_sig \u0026lt; 0: self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.data.close[0]) self.sell(size=self.p.stake) def runstrategy(): args = parse_args() # Create a cerebro cerebro = bt.Cerebro() # Get the dates from the args fromdate = datetime.datetime.strptime(args.fromdate, \u0026#39;%Y-%m-%d\u0026#39;) todate = datetime.datetime.strptime(args.todate, \u0026#39;%Y-%m-%d\u0026#39;) # Create the 1st data data = btfeeds.BacktraderCSVData( dataname=args.data, fromdate=fromdate, todate=todate) # Add the 1st data to cerebro cerebro.adddata(data) # Add a strategy cerebro.addstrategy(SMACrossOver, period=args.period, stake=args.stake) # Add the commission - only stocks like a for each operation cerebro.broker.setcash(args.cash) commtypes = dict( none=None, perc=bt.CommInfoBase.COMM_PERC, fixed=bt.CommInfoBase.COMM_FIXED) # Add the commission - only stocks like a for each operation cerebro.broker.setcommission(commission=args.comm, mult=args.mult, margin=args.margin, percabs=not args.percrel, commtype=commtypes[args.commtype], stocklike=args.stocklike) # And run it cerebro.run() # Plot if requested if args.plot: cerebro.plot(numfigs=args.numfigs, volume=False) def parse_args(): parser = argparse.ArgumentParser( description=\u0026#39;Commission schemes\u0026#39;, formatter_class=argparse.ArgumentDefaultsHelpFormatter,) parser.add_argument(\u0026#39;--data\u0026#39;, \u0026#39;-d\u0026#39;, default=\u0026#39;../../datas/2006-day-001.txt\u0026#39;, help=\u0026#39;data to add to the system\u0026#39;) parser.add_argument(\u0026#39;--fromdate\u0026#39;, \u0026#39;-f\u0026#39;, default=\u0026#39;2006-01-01\u0026#39;, help=\u0026#39;Starting date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, \u0026#39;-t\u0026#39;, default=\u0026#39;2006-12-31\u0026#39;, help=\u0026#39;Starting date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--stake\u0026#39;, default=1, type=int, help=\u0026#39;Stake to apply in each operation\u0026#39;) parser.add_argument(\u0026#39;--period\u0026#39;, default=30, type=int, help=\u0026#39;Period to apply to the Simple Moving Average\u0026#39;) parser.add_argument(\u0026#39;--cash\u0026#39;, default=10000.0, type=float, help=\u0026#39;Starting Cash\u0026#39;) parser.add_argument(\u0026#39;--comm\u0026#39;, default=2.0, type=float, help=(\u0026#39;Commission factor for operation, either a\u0026#39; \u0026#39;percentage or a per stake unit absolute value\u0026#39;)) parser.add_argument(\u0026#39;--mult\u0026#39;, default=10, type=int, help=\u0026#39;Multiplier for operations calculation\u0026#39;) parser.add_argument(\u0026#39;--margin\u0026#39;, default=2000.0, type=float, help=\u0026#39;Margin for futures-like operations\u0026#39;) parser.add_argument(\u0026#39;--commtype\u0026#39;, required=False, default=\u0026#39;none\u0026#39;, choices=[\u0026#39;none\u0026#39;, \u0026#39;perc\u0026#39;, \u0026#39;fixed\u0026#39;], help=(\u0026#39;Commission - choose none for the old\u0026#39; \u0026#39; CommissionInfo behavior\u0026#39;)) parser.add_argument(\u0026#39;--stocklike\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;If the operation is for stock-like assets or\u0026#39; \u0026#39;future-like assets\u0026#39;)) parser.add_argument(\u0026#39;--percrel\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;If perc is expressed in relative xx% rather\u0026#39; \u0026#39;than absolute value 0.xx\u0026#39;)) parser.add_argument(\u0026#39;--plot\u0026#39;, \u0026#39;-p\u0026#39;, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;Plot the read data\u0026#39;) parser.add_argument(\u0026#39;--numfigs\u0026#39;, \u0026#39;-n\u0026#39;, default=1, help=\u0026#39;Plot using numfigs figures\u0026#39;) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrategy() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/11-commission-schemes/02-commission-schemes-extending/","section":"教程","summary":"佣金及相关功能由一个单一的类 CommissionInfo 管理，通常通过调用 broker.setcommission 实例化。\n这个概念仅限于带有保证金和每份合约固定佣金的期货，以及基于价格/数量百分比佣金的股票。虽然不是最灵活的方案，但它已经发挥了作用。\n","title":"自定义佣金方案扩展","type":"docs"},{"content":"在这个页面上，我们回答了一些关于Fyne主题和控件设计的常见问题。\n自定义 # 问：如何更改Label控件文本的颜色？\n答： 所有标准控件都使用当前的Theme定义来设置颜色、字体和大小。要对你的应用程序进行更改，请考虑使用自定义主题。\n如果你的应用程序需要不同颜色的文本，可以使用canvas.Text类型代替。这允许直接设置文本的颜色和大小。在执行此操作时要小心，因为用户可以在浅色或深色主题变体之间选择，所以你应该在两种情况下都进行测试。\n问：如何从我的Entry控件中移除背景颜色？\n答： 输入背景是由主题的InputBackground颜色设置的。你可以将其更改为color.Transparent以移除所有输入背景框。不可能编辑单个输入元素的样式——主题API旨在提供可自定义的，但一致的设计。\n主题API # 问：如何使用我在v2.0.0之前编写的自定义主题？\n答： 随着时间的推移，你应该考虑更新使用新的主题API。然而，它是可能的在过渡时期使用一个简单的适配器来允许旧主题的使用。你会发现theme.FromLegacy函数，可以将旧的主题实例适配到新的API。\nmyTheme := \u0026amp;myOldThemeType{} updated := theme.FromLegacy(myTheme) app.Settings().SetTheme(updated) 使用这种模式下的主题时，没有性能损失，但在未来的版本中这个API将被移除。\n","date":"2025-05-29","externalUrl":null,"permalink":"/docs/gofyne/10-faq/02-theme/","section":"教程","summary":"在这个页面上，我们回答了一些关于Fyne主题和控件设计的常见问题。\n自定义 # 问：如何更改Label控件文本的颜色？\n","title":"主题与自定义","type":"docs"},{"content":"Fyne 完全使用矢量图形构建，这意味着用 Fyne 编写的应用程序可以美观地缩放到任何大小（不仅仅是整数增量）。这对于移动设备和高端计算机上越来越受欢迎的高密度显示屏是一个巨大的好处。默认的缩放值是根据你的操作系统计算出来的 - 在一些系统上，这是用户配置，在其他系统上则来自于你的屏幕像素密度（DPI - 每英寸点数）。如果一个 Fyne 窗口被移动到另一个屏幕上，它将重新缩放并相应地调整窗口大小！我们称之为“自动缩放”，旨在在更换显示器时保持应用程序用户界面的相同大小。\n你可以使用 fyne_settings 应用程序调整应用程序的大小，或者通过设置 FYNE_SCALE 环境变量来设置特定的缩放比例。这些值可以使内容比系统设置大或小，使用 \u0026ldquo;1.5\u0026rdquo; 会使事物变大 50%，设置 0.8 会使其缩小 20%。\nStandard size FYNE_SCALE=0.5 FYNE_SCALE=2.5 ","date":"2025-05-14","externalUrl":null,"permalink":"/docs/gofyne/09-architecture/02-scaling/","section":"教程","summary":"Fyne 完全使用矢量图形构建，这意味着用 Fyne 编写的应用程序可以美观地缩放到任何大小（不仅仅是整数增量）。这对于移动设备和高端计算机上越来越受欢迎的高密度显示屏是一个巨大的好处。默认的缩放值是根据你的操作系统计算出来的 - 在一些系统上，这是用户配置，在其他系统上则来自于你的屏幕像素密度（DPI - 每英寸点数）。如果一个 Fyne 窗口被移动到另一个屏幕上，它将重新缩放并相应地调整窗口大小！我们称之为“自动缩放”，旨在在更换显示器时保持应用程序用户界面的相同大小。\n","title":"缩放 Scaling","type":"docs"},{"content":"标准控件与 Fyne 一起提供，旨在支持标准用户交互和需求。由于 GUI 经常需要提供自定义功能，因此可能需要编写自定义控件。本文概述了如何进行。\n一个控件被分为两个区域 - 每个都实现一个标准接口 - fyne.Widget 和 fyne.WidgetRenderer。控件定义行为和状态，而渲染器用于定义它应如何绘制到屏幕上。\nfyne.Widget # Fyne 中的控件简单来说是一个有状态的画布对象，其渲染定义与主逻辑分离。从 fyne.Widget 接口可以看出，必须实现的内容并不多。\ntype Widget interface { CanvasObject CreateRenderer() WidgetRenderer } 由于控件需要像任何其他画布项目一样被使用，我们从相同的接口继承。为了省去编写所有必需的函数，我们可以使用 widget.BaseWidget 类型，它处理了基础内容。\n每个控件定义将包含比接口要求更多的内容。在 Fyne 控件中导出定义行为的字段是标准做法（就像 canvas 包中定义的原语一样）。\n例如，看看 widget.Button 类型：\ntype Button struct { BaseWidget Text string Style ButtonStyle Icon fyne.Resource OnTapped func() } 你可以看到这些项目如何存储有关控件行为的状态，但没有关于它如何呈现的信息。\nfyne.WidgetRenderer # 控件渲染器负责管理一组 fyne.CanvasObject 原语，这些原语组合在一起创建了我们控件的设计。它很像 fyne.Container，但有自定义布局和一些额外的主题处理。\n每个控件都必须提供一个渲染器，但完全可以重用另一个控件的渲染器 - 特别是如果你的控件是另一个标准控件的轻量级包装。\ntype WidgetRenderer interface { Layout(Size) MinSize() Size Refresh() Objects() []CanvasObject Destroy() } 可以看到 Layout(Size) 和 MinSize() 函数类似于 fyne.Layout 接口，但没有 []fyne.CanvasObject 参数 - 这是因为控件确实需要布局，但它控制哪些对象将被包含。\nRefresh() 方法在绘制的控件发生变化或主题被更改时触发。在任一情况下，我们可能需要调整它的外观。最后，当这个渲染器不再需要时，会调用 Destroy() 方法，因此它应该清除任何可能泄漏的资源。\n再次与按钮控件比较 - 它的 fyne.WidgetRenderer 实现基于以下类型：\ntype buttonRenderer struct { icon *canvas.Image label *canvas.Text shadow *fyne.CanvasObject objects []fyne.CanvasObject button *Button } 可以看到它有字段来缓存实际图像、文本和阴影画布对象用于绘制。它跟踪 fyne.WidgetRenderer 所需的对象切片以方便使用。\n最后它保留对 widget.Button 的引用以获取所有状态信息。在 Refresh() 方法中，它将根据 widget.Button 类型中的任何更改更新图形状态。\n整合 # 基本的控件将扩展 widget.BaseWidget 类型并声明控件持有的任何状态。CreateRenderer() 函数必须存在并返回一个新的 fyne.WidgetRenderer 实例。Fyne 中的控件和驱动代码将确保这被相应地缓存 - 如果\n","date":"2025-04-23","externalUrl":null,"permalink":"/docs/gofyne/08-extend/02-custom-widget/","section":"教程","summary":"标准控件与 Fyne 一起提供，旨在支持标准用户交互和需求。由于 GUI 经常需要提供自定义功能，因此可能需要编写自定义控件。本文概述了如何进行。\n一个控件被分为两个区域 - 每个都实现一个标准接口 - fyne.Widget 和 fyne.WidgetRenderer。控件定义行为和状态，而渲染器用于定义它应如何绘制到屏幕上。\n","title":"自定义控件 Widget","type":"docs"},{"content":"绑定控件的最简单形式是将绑定项作为值传递给它，而不是静态值。许多控件提供了一个WithData构造函数，它将接受一个类型化的数据绑定项。要设置绑定，你需要做的就是传递正确的类型。\n尽管在初始代码中这看起来可能没有多大好处，但你可以看到它如何确保显示的内容始终与数据源保持最新。你会注意到，我们不需要在Label控件上调用Refresh()，甚至不需要保留它的引用，但它仍然会适当更新。\npackage main import ( \u0026#34;time\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/data/binding\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;Simple\u0026#34;) str := binding.NewString() str.Set(\u0026#34;Initial value\u0026#34;) text := widget.NewLabelWithData(str) w.SetContent(text) go func() { time.Sleep(time.Second * 2) str.Set(\u0026#34;A new string\u0026#34;) }() w.ShowAndRun() } 在下一步中，我们将看看如何通过双向绑定设置编辑值的控件。\n","date":"2025-04-05","externalUrl":null,"permalink":"/docs/gofyne/07-binding/01-simple/","section":"教程","summary":"绑定控件的最简单形式是将绑定项作为值传递给它，而不是静态值。许多控件提供了一个WithData构造函数，它将接受一个类型化的数据绑定项。要设置绑定，你需要做的就是传递正确的类型。\n","title":"简单绑定","type":"docs"},{"content":" 表格 Table # Table集合控件类似于List控件（工具包的另一个集合控件），它具有二维索引。像List一样，这旨在帮助构建性能非常高的接口，当展示大量数据时。因此，控件不是用所有数据创建的，而是在需要时调用数据源。\nTable使用回调函数在需要数据时请求数据。有3个主要的回调，Length、CreateCell和UpdateCell。Length回调（首先传递）是最简单的，它返回要展示的数据中有多少项，它返回的两个int分别代表行数和列数。其他两个与内容模板相关。\nCreateCell回调返回一个新的模板对象，就像列表一样。不同之处在于MinSize将定义每个单元格的标准大小，以及表格的最小大小（它至少显示一个单元格）。如前所述，UpdateCell被调用来将数据应用于单元格模板。传入的索引是相同的(row, col)int对。\npackage main import ( \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) var data = [][]string{[]string{\u0026#34;左上角\u0026#34;, \u0026#34;右上角\u0026#34;}, []string{\u0026#34;左下角\u0026#34;, \u0026#34;右下角\u0026#34;}} func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;表格控件\u0026#34;) list := widget.NewTable( func() (int, int) { return len(data), len(data[0]) }, func() fyne.CanvasObject { return widget.NewLabel(\u0026#34;宽内容\u0026#34;) }, func(i widget.TableCellID, o fyne.CanvasObject) { o.(*widget.Label).SetText(data[i.Row][i.Col]) }) myWindow.SetContent(list) myWindow.ShowAndRun() } ","date":"2025-03-24","externalUrl":null,"permalink":"/docs/gofyne/06-collection/02-table/","section":"教程","summary":"表格 Table # Table集合控件类似于List控件（工具包的另一个集合控件），它具有二维索引。像List一样，这旨在帮助构建性能非常高的接口，当展示大量数据时。因此，控件不是用所有数据创建的，而是在需要时调用数据源。\n","title":"表格 Table","type":"docs"},{"content":"按钮控件可以包含文本、图标或两者，构造函数是 widget.NewButton() 和 widget.NewButtonWithIcon()。要创建一个文本按钮，只有两个参数，string 内容和一个没有参数的 func()，当按钮被点击时将调用此函数。参见示例以了解如何创建它。\n带有图标的按钮构造函数包含一个额外的参数，即包含图标数据的 fyne.Resource。theme 包中的内置图标都适当地适应主题更改。如果将自己的图像加载为资源，你可以传入自己的图像 - 诸如 fyne.LoadResourceFromPath() 的助手可能会有所帮助，尽可能推荐捆绑资源。\n要创建仅带图标的按钮，你应该将 \u0026quot;\u0026quot; 作为标签参数传递给 widget.NewButtonWithIcon()。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; //\u0026#34;fyne.io/fyne/v2/theme\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;按钮控件\u0026#34;) content := widget.NewButton(\u0026#34;点击我\u0026#34;, func() { log.Println(\u0026#34;点击\u0026#34;) }) //content := widget.NewButtonWithIcon(\u0026#34;首页\u0026#34;, theme.HomeIcon(), func() { //\tlog.Println(\u0026#34;点击首页\u0026#34;) //}) myWindow.SetContent(content) myWindow.ShowAndRun() } ","date":"2025-02-28","externalUrl":null,"permalink":"/docs/gofyne/05-widget/02-button/","section":"教程","summary":"按钮控件可以包含文本、图标或两者，构造函数是 widget.NewButton() 和 widget.NewButtonWithIcon()。要创建一个文本按钮，只有两个参数，string 内容和一个没有参数的 func()，当按钮被点击时将调用此函数。参见示例以了解如何创建它。\n","title":"按钮 Button","type":"docs"},{"content":"网格布局将容器的元素以网格模式布置，具有固定数量的列。项目将填充单个行，直到达到列数，之后将创建新行。垂直空间将在对象的每一行之间平均分配。\n使用 layout.NewGridLayout(cols) 创建网格布局，其中 cols 是您希望每行中有的项目（列）数量。然后将此布局作为第一个参数传递给 container.New(...)。\n如果您调整容器的大小，则每个单元格将平等地调整大小，以分享可用空间。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/layout\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;网格布局\u0026#34;) text1 := canvas.NewText(\u0026#34;1\u0026#34;, color.White) text2 := canvas.NewText(\u0026#34;2\u0026#34;, color.White) text3 := canvas.NewText(\u0026#34;3\u0026#34;, color.White) grid := container.New(layout.NewGridLayout(2), text1, text2, text3) myWindow.SetContent(grid) myWindow.ShowAndRun() } ","date":"2025-02-01","externalUrl":null,"permalink":"/docs/gofyne/04-container/02-grid/","section":"教程","summary":"网格布局将容器的元素以网格模式布置，具有固定数量的列。项目将填充单个行，直到达到列数，之后将创建新行。垂直空间将在对象的每一行之间平均分配。\n使用 layout.NewGridLayout(cols) 创建网格布局，其中 cols 是您希望每行中有的项目（列）数量。然后将此布局作为第一个参数传递给 container.New(...)。\n","title":"网格 Grid","type":"docs"},{"content":"canvas.Text 用于 Fyne 内的所有文本渲染。它通过指定文本和文本颜色来创建。文本使用当前主题指定的默认字体渲染。\n文本对象允许某些配置，如 Alignment 和 TextStyle 字段，如此示例中所示。如果你想使用等宽字体，可以指定 fyne.TextStyle{Monospace: true}。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;文本\u0026#34;) text := canvas.NewText(\u0026#34;文本对象\u0026#34;, color.White) text.Alignment = fyne.TextAlignTrailing text.TextStyle = fyne.TextStyle{Italic: true} w.SetContent(text) w.ShowAndRun() } 通过指定 FYNE_FONT 环境变量，可以使用另一种字体。使用这个来设置一个 .ttf 文件，代替 Fyne 工具包或当前主题提供的字体。\n","date":"2025-01-05","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/02-text/","section":"教程","summary":"canvas.Text 用于 Fyne 内的所有文本渲染。它通过指定文本和文本颜色来创建。文本使用当前主题指定的默认字体渲染。\n文本对象允许某些配置，如 Alignment 和 TextStyle 字段，如此示例中所示。如果你想使用等宽字体，可以指定 fyne.TextStyle{Monospace: true}。\n","title":"文本 Text","type":"docs"},{"content":"在前一个示例中，我们看到了如何将CanvasObject设置为Canvas的内容，但只显示一个视觉元素并不是很有用。要显示多个项，我们使用Container类型。\n由于fyne.Container也是一个fyne.CanvasObject，我们可以将它设置为fyne.Canvas的内容。在这个示例中，我们创建了3个文本对象，然后使用container.NewWithoutLayout()函数将它们放入一个容器中。由于没有设置布局，我们可以像你看到的那样用text2.Move()移动元素。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; //\u0026#34;fyne.io/fyne/v2/layout\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Container\u0026#34;) green := color.NRGBA{R: 0, G: 180, B: 0, A: 255} text1 := canvas.NewText(\u0026#34;Hello\u0026#34;, green) text2 := canvas.NewText(\u0026#34;There\u0026#34;, green) text2.Move(fyne.NewPos(20, 20)) content := container.NewWithoutLayout(text1, text2) // content := container.New(layout.NewGridLayout(2), text1, text2) myWindow.SetContent(content) myWindow.ShowAndRun() } fyne.Layout实现了一种在容器内组织项目的方法。通过在这个示例中取消注释container.New()行，你改变了容器以使用2列的网格布局。运行此代码并尝试调整窗口大小，看看布局如何自动配置窗口的内容。还要注意，text2的手动位置被布局代码忽略了。\n要了解更多，你可以查看布局组件。\n","date":"2024-11-30","externalUrl":null,"permalink":"/docs/gofyne/02-explore/02-container/","section":"教程","summary":"在前一个示例中，我们看到了如何将CanvasObject设置为Canvas的内容，但只显示一个视觉元素并不是很有用。要显示多个项，我们使用Container类型。\n","title":"容器与布局","type":"docs"},{"content":"","date":"2024-11-24","externalUrl":null,"permalink":"/docs/gofyne/02-explore/","section":"教程","summary":"","title":"探索 Fyne","type":"docs"},{"content":"在完成了入门安装文档中的步骤后，你现在已经准备好构建你的第一个应用程序了。为了说明这个过程，我们将构建一个简单的“Hello World”应用程序。\n一个简单的应用程序从使用app.New()创建一个应用实例开始，然后使用app.NewWindow()打开一个窗口。接着定义一个控件树，并使用窗口上的SetContent()将其设置为主内容。然后通过在窗口上调用ShowAndRun()来显示应用UI。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;Hello World\u0026#34;) w.SetContent(widget.NewLabel(\u0026#34;Hello World!\u0026#34;)) w.ShowAndRun() } 上面的代码可以使用命令go build .进行构建，然后通过运行hello命令或双击图标来执行。你也可以跳过编译步骤，直接使用go run ..来运行代码。\n无论采取哪种方法，都会显示一个窗口，看起来像这样：\n如果你更喜欢浅色主题，只需设置环境变量FYNE_THEME=light，你就会得到：\n这就是入门的全部内容了。要了解更多，你可以阅读完整的 API文档。\n","date":"2024-10-19","externalUrl":null,"permalink":"/docs/gofyne/01-started/02-hello/","section":"教程","summary":"在完成了入门安装文档中的步骤后，你现在已经准备好构建你的第一个应用程序了。为了说明这个过程，我们将构建一个简单的“Hello World”应用程序。\n","title":"创建第一个应用","type":"docs"},{"content":"本文将介绍三个高效搜索命令，分别是 fd、ripgrep 与 fzf。fd 和 ripgrep 对标的是传统 grep 和 find，它们在性能和体验上有大幅提升。如果你想成为 10x 程序员，强烈推荐使用它们。\nfd # fd 是一款文件查找命令，可替换系统默认 find。它的体验更友好，且查询效率极高。使用传统的 find 时，经常要查手册看帮助文档，但使用 fd，它的默认行为就能满足我们大部分的需求。\n安装 # # macOS brew install fd # Debian/Ubuntu sudo apt install fd-find 基础使用 # 与 find 对比一下就知道了：\n# find 查找 Python 文件 find . -name \u0026#34;*.py\u0026#34; -type f # fd 查找 Python 文件（默认递归，无需 -name -type） fd \u0026#34;*.py\u0026#34; # find 搜索包含 \u0026#34;main\u0026#34; 的文件 find . -type f -name \u0026#34;*main*\u0026#34; # fd 搜索包含 \u0026#34;main\u0026#34; 的文件 fd main fd 的默认行为比 find 聪明得多：\n自动递归搜索，不用像 find 那样手动 -type f 默认忽略隐藏文件和 .gitignore 中的文件（搜项目代码时不会带出 node_modules） 彩色高亮匹配内容 正则搜索 # fd \u0026#34;^config\\.\u0026#34; # 搜索以 config. 开头的文件 fd \u0026#34;[0-9]{4}\u0026#34; # 搜索包含 4 位数字的文件 指定搜索路径 # fd pattern ~/Projects # 在指定目录下搜索 fd -H pattern ~ # 包含隐藏文件 搭配命令执行 # fd 真正的威力在于配合 -x 或 -X 执行命令：\n# 查找所有 Go 文件并统计行数 fd -e go -x wc -l # 查找并删除所有 .tmp 文件 fd -e tmp -X rm # 查找并替换文件中的内容 fd -e md -x sed -i \u0026#39;s/old/new/g\u0026#39; -x 对每个匹配的文件分别执行，-X 一次性传入所有匹配的文件。\n文件类型过滤 # fd -e py # 只搜索 Python 文件 fd -e md -e txt # 搜索 Markdown 和文本文件 fd -t f # 只搜索文件 fd -t d # 只搜索目录 fd -t l # 只搜索符号链接 性能 # fd 使用 Rust 编写，通过并行遍历文件系统和智能跳过忽略文件，搜索速度比 find 快 5-10 倍。\nripgrep # ripgrep 是一款文本搜索命令，功能与 grep 类似，在体验和性能上完胜 grep。\n安装 # brew install ripgrep 基础使用 # ripgrep 默认递归搜索，命令为 rg pattern，且默认高亮显示，比 grep --color main . -nR 简洁得多：\nrg main ~/Code/golang-examples/ 通配符 # 使用 -g 通过通配符指定搜索路径：\nrg main -g \u0026#39;!main.go\u0026#39; # 排除指定文件 rg -g \u0026#39;!directory\u0026#39; # 排除指定目录 rg -g \u0026#39;!/*/*\u0026#39; # 禁用目录递归 正则 # rg -e \u0026#39;[0-9]{2}:[0-9]{2}\u0026#39; # 搜索日期格式 默认过滤 # 和 fd 一样，ripgrep 高性能的原因之一也是默认忽略隐藏文件和 .gitignore、.rgignore 中的文件。禁用忽略用 --no-ignore。搜索隐藏文件用 --hidden。彻底禁用过滤用 -uuu。\n大小写 # rg pattern # 默认大小写敏感 rg -i pattern # 忽略大小写 rg -S pattern # smartcase 模式 搜索并替换 # rg main -r new_content # 替换搜索结果，不修改原文件 配置 # 通过环境变量 RIPGREP_CONFIG_PATH 指定配置文件：\n--max-columns=150 --max-columns-preview --smart-case --hidden --glob=!.git/* fzf # fzf 全名 fuzzy finder，是一款通用的命令行模糊查找工具。它可以与其他命令结合，堪称终端管道中的瑞士军刀。\n安装 # # macOS brew install fzf # 安装额外的小工具（Ctrl+T、Alt+C 等绑定） $(brew --prefix)/opt/fzf/install 核心概念 # fzf 的核心是一个交互式模糊搜索界面：\ncat /usr/share/dict/words | fzf 输入几个字母，fzf 会模糊匹配所有包含这些字母的行，按匹配度排序并实时高亮。\n文件搜索 # fzf 与 fd 结合，效果爆炸：\n# 交互式搜索文件，选中后用 nvim 打开 fd --type f | fzf | xargs nvim # 或用 Ctrl+T（需要 source 补全脚本） 目录跳转 # # 一行命令实现交互式 cd cd $(fd --type d | fzf) # 封装成函数 function fcd() { local dir dir=$(fd --type d --hidden --follow --exclude .git | fzf) \u0026amp;\u0026amp; cd \u0026#34;$dir\u0026#34; } 历史命令搜索 # 在 ~/.zshrc 中加一行，替换默认的 Ctrl+R：\nsource \u0026lt;(fzf --zsh) 之后按 Ctrl+R 弹出交互式搜索框，输入关键字模糊搜索历史命令，选中后回车执行。比 bash 默认翻页式搜索直观得多。\n进程搜索 # kill -9 $(ps aux | fzf | awk \u0026#39;{print $2}\u0026#39;) Git 集成 # # 交互式切换分支 git checkout $(git branch | fzf | tr -d \u0026#39; *\u0026#39;) # 交互式查看 git 日志（带预览） git log --oneline --graph | fzf --preview \u0026#39;git show {1}\u0026#39; --preview 是 fzf 的强力功能——选中时自动预览选中项的内容，不用打开额外窗口。\n","date":"2024-04-14","externalUrl":null,"permalink":"/docs/mytermenv/commands/search/","section":"教程","summary":"本文将介绍三个高效搜索命令，分别是 fd、ripgrep 与 fzf。fd 和 ripgrep 对标的是传统 grep 和 find，它们在性能和体验上有大幅提升。如果你想成为 10x 程序员，强烈推荐使用它们。\n","title":"搜索查找","type":"docs"},{"content":"在终端启动时显示系统信息，既能了解当前环境状态，也让终端更有\u0026quot;个性\u0026quot;。社区中有几款流行的系统信息展示工具，各有特色。\nneofetch # neofetch 是最经典的系统信息工具，它以 ASCII 艺术图 + 系统信息的形式展示。不过 neofetch 已于 2020 年停止维护，原作者建议使用其他替代品。\n安装 # # macOS brew install neofetch # Debian/Ubuntu sudo apt install neofetch 使用 # neofetch 输出示例：\n██████████████ poloxue@macbook ████████████████ ----------------- ██╣ █████ ██ OS: macOS 14.3 (Sonoma) ███╣ ██ ║████ Host: MacBook Pro M3 Pro ███╣ ║ █████ Kernel: Darwin 23.2.0 ████║ ██████ Uptime: 3d 12h 34m █╣ ██║ ║██║ ██ Packages: 342 (brew) █╣ ║█║ ║ ██║ ██ Shell: zsh 5.9 ████████████████████ Resolution: 3456x2234 ████████████████ Terminal: iTerm2 3.5 自定义 # neofetch 的配置文件在 ~/.config/neofetch/config.conf。你可以自定义：\n显示哪些信息（关闭不关心的项） ASCII 艺术图的样式和颜色 信息的排列顺序 例如，只显示最关键的信息：\n# ~/.config/neofetch/config.conf print_info() { info title info \u0026#34;OS\u0026#34; distro info \u0026#34;Kernel\u0026#34; kernel info \u0026#34;Uptime\u0026#34; uptime info \u0026#34;Packages\u0026#34; packages info \u0026#34;Shell\u0026#34; shell } fastfetch # fastfetch 是 neofetch 的 Rust 重写版，速度和兼容性都有显著提升。如果你觉得 neofetch 启动太慢，fastfetch 是绝佳替代品。\n安装 # # macOS brew install fastfetch # 其他系统 # 从 GitHub Releases 下载二进制 使用 # fastfetch fastfetch 默认输出比 neofetch 更现代，支持 TrueColor 和更丰富的图标。\n自定义 # fastfetch 支持 JSON 配置文件，可精细控制每个模块的显示：\n# 生成默认配置文件 fastfetch --gen-config # 只显示指定模块 fastfetch --os --kernel --shell --terminal 配合欢迎消息 # 将 fastfetch 添加到 ~/.zshrc 末尾即可每次打开终端自动显示：\n# ~/.zshrc fastfetch pfetch # pfetch 是同一作者（dylanaraps）用纯 Bash 编写的极简系统信息工具。它没有外部依赖，脚本体积不到 2KB，启动速度极快。\n安装 # # 直接下载脚本 curl -LO https://raw.githubusercontent.com/dylanaraps/pfetch/main/pfetch chmod +x pfetch # 移动到 PATH mv pfetch /usr/local/bin/ 使用 # pfetch 输出风格非常简洁，没有 ASCII 艺术图，只用纯文字和颜色展示系统信息：\npoloxue@macbook -------------- OS macOS 14.3 Kernel Darwin 23.2.0 Uptime 3d 12h Packages 342 Shell zsh 优势 # 零依赖——纯 Bash，任何系统都能跑 极快——启动时间几乎可以忽略 代码可读——整个脚本不到 100 行，可以当 Bash 学习素材 可定制——直接修改脚本即可调整显示内容 三者对比 # 工具 语言 速度 依赖 维护状态 适合场景 neofetch Bash 较慢 少 已停维 经典选择，要 ASCII 图 fastfetch Rust 极快 无 活跃 日常推荐，功能最全 pfetch Bash 极快 无 已停维 极简主义，追求速度 推荐方案 # 从实际使用角度，推荐以下组合：\n# ~/.zshrc # 先显示一条自定义欢迎消息 echo \u0026#34;🚀 欢迎回来，$(whoami)！美好的 $(date \u0026#39;+%Y-%m-%d\u0026#39;) 开始了～\u0026#34; echo \u0026#34;\u0026#34; # 再用 fastfetch 显示系统信息 fastfetch 这样既有温度（自定义欢迎语），又有信息量（系统状态），而且 fastfetch 的启动速度足够快，不会让人等得不耐烦。\n","date":"2024-04-14","externalUrl":null,"permalink":"/docs/mytermenv/startup/fetch/","section":"教程","summary":"在终端启动时显示系统信息，既能了解当前环境状态，也让终端更有\"个性\"。社区中有几款流行的系统信息展示工具，各有特色。\nneofetch # neofetch 是最经典的系统信息工具，它以 ASCII 艺术图 + 系统信息的形式展示。不过 neofetch 已于 2020 年停止维护，原作者建议使用其他替代品。\n","title":"系统信息工具","type":"docs"},{"content":"重点来了，接下来我们一起来看看 zsh 的效率神器 - 插件能力吧。本文先给大家推荐 5 款常用的插件。oh-my-zsh 提供的所有内置插件，都可以在仓库 ohmyzsh/ohmyzsh/plugins 中找到，每个插件都有相应的介绍文档。\n本文介绍的 5 个插件：\n插件 作用 git 大量 git 别名，敲 3 个字母代替 10 个 sudo 双击 Esc 在命令前加 sudo extract x 解压任何格式的压缩包 web-search 终端里直接搜索搜索引擎 colored-man-pages man 页面语法高亮 启用插件很简单，编辑 ~/.zshrc，找到 plugins=(git) 这一行，加入需要的插件名：\nplugins=(git sudo extract web-search colored-man-pages) 然后执行 source ~/.zshrc 生效。\ngit # 推荐优先级最高，必装插件。它提供了一百多个 git 别名，日常操作从 3-5 个字母变成 1-2 个：\n别名 原命令 说明 gst git status 查看状态 ga git add 添加文件 gcmsg git commit -m 提交 gp git push 推送 gl git pull 拉取 gco git checkout 切换分支 gb git branch 分支管理 gd git diff 查看差异 glog git log --oneline --decorate --graph 精美日志 gsta git stash 暂存 gcf git config --list 查看配置 有了这些别名，从此告别敲完整的 git 命令。\nsudo # 有的命令敲完了才发现忘了加 sudo——这个插件就是为了解决这个问题。在任意命令上双击 Esc，自动在命令前加上 sudo：\n# 输入 systemctl restart nginx # 双击 Esc 后变成 sudo systemctl restart nginx 不用回退到行首加 sudo，省一次 Home 键。\nextract # 解压文件最烦的就是记参数——tar -xzf 还是 tar -xf？unzip 还是 unrar？这个插件提供一个统一的 x 命令：\nx file.tar.gz # 自动识别格式解压 x file.zip # 同上 x file.rar # 同上 x file.tar.xz # 同上 它支持 tar.gz、tar.bz2、tar.xz、zip、rar、7z 等几乎所有常见压缩格式，无需记忆参数。\nweb-search # 在终端里直接搜索，不用切到浏览器：\n# 搜索 Google google how to exit vim # 搜索 GitHub github zsh plugin # 搜索 Wikipedia wiki zshell # 指定搜索引擎 open \u0026#34;https://stackoverflow.com/search?q=zsh+plugin\u0026#34; colored-man-pages # 给 man 命令加上语法高亮，让手册页面更容易阅读。开启后，代码块、选项、关键字都会有不同的颜色标识，对比黑白灰的原始 man 页面，体验提升一个档次。\n小结 # 以上 5 个插件是 oh-my-zsh 的基础入门推荐。git 和 sudo 是日常必装，extract 和 web-search 能省去很多重复记忆，colored-man-pages 让读文档更友好。下一篇继续介绍更多基础插件。\n","date":"2024-02-25","externalUrl":null,"permalink":"/docs/mytermenv/ohmyzsh/basicplugins/","section":"教程","summary":"重点来了，接下来我们一起来看看 zsh 的效率神器 - 插件能力吧。本文先给大家推荐 5 款常用的插件。oh-my-zsh 提供的所有内置插件，都可以在仓库 ohmyzsh/ohmyzsh/plugins 中找到，每个插件都有相应的介绍文档。\n","title":"基础插件","type":"docs"},{"content":"开始前，先问为什么，知其然，要知其所以然，是个好习惯。所以，为什么要用 zsh 呢？\n大家最熟悉的 shell 解释器，肯定是 bash。zsh（Z Shell）相对于 bash（Bourne Again Shell）有哪些优势呢？\n开箱即用的体验提升 # zsh 的默认行为就已经比 bash 友好很多：\n智能补全 # 在 bash 里按 Tab 只是简单补全文件名。但在 zsh 中：\n输入 cd 然后按 Tab→ 列出目录，再次 Tab 可进入子目录选择 输入 git 按 Tab→ 列出所有 git 子命令 输入 ssh 按 Tab→ 列出 ~/.ssh/config 中的主机 输入 kill 按 Tab→ 列出进程列表 拼写纠正 # 你输入 cd Documetns，zsh 会问你是不是想进 Documents？输入 sl 会提示是否要执行 ls？这个功能拯救过我不计其数的笔误。\n更强大的通配符 # # 递归匹配所有 .py 文件（** 表示任意层目录） ls **/*.py # 排除某些文件 ls *.txt~readme.txt # 匹配特定范围 ls file[1-10].txt 目录别名 # 不用 cd 就能切换目录：\n# 直接输入目录名即可进入 /usr/local/bin # 使用 ~ 加关键字快速跳转 ~doc # 等价于 cd ~/Documents 极致可定制 # zsh 的提示符（prompt）支持完善的样式定制，甚至可以右侧显示信息：\n# 在 .zshrc 中设置右侧提示符显示 git 分支 RPROMPT=\u0026#39;$(git_branch)\u0026#39; 结合 Powerlevel10k 主题，你能得到一个信息丰富又美观的提示符——显示命令执行时间、git 状态、Python 虚拟环境、后台 Job 数量等。\n丰富的插件生态 # oh-my-zsh 社区贡献了 300+ 内置插件，几乎覆盖了所有常用工具：\ngit — git 命令别名，gst= git status, gco= git checkout docker — docker 命令补全 pip — pip 补全 brew — Homebrew 补全 sudo — 双击 Esc 在命令前加 sudo 不用自己去 .bashrc 里写一堆 alias，装个插件就全有了。\n兼容性 # zsh 在语法上几乎完全兼容 bash——你的 .bashrc、.bash_profile 里的内容可以直接搬到 .zshrc 里用。迁移成本极低，但收益巨大。\n小结 # 如果你还在用 bash，切换到 zsh 是性价比最高的终端效率提升——零成本、零风险，但每天都能感受到的体验提升。下一章我们通过 oh-my-zsh 把 zsh 武装起来。\n","date":"2024-02-11","externalUrl":null,"permalink":"/docs/mytermenv/zsh/why/","section":"教程","summary":"开始前，先问为什么，知其然，要知其所以然，是个好习惯。所以，为什么要用 zsh 呢？\n大家最熟悉的 shell 解释器，肯定是 bash。zsh（Z Shell）相对于 bash（Bourne Again Shell）有哪些优势呢？\n","title":"为什么选择 zsh ？","type":"docs"},{"content":"本节介绍 iTerm2 安装与主题。\n安装 # 安装 iTerm2 很简单，直接去官网 iterm2.com 下载安装包，或者用 Homebrew：\nbrew install --cask iterm2 安装完成后，打开 iTerm2，你会看到默认的黑底白字界面。别急，我们先给它换个好看的主题。\n主题配置 # iTerm2 支持丰富的主题配色，社区维护了大量配色方案。推荐几个经典主题：\n内置主题 # iTerm2 内置了 100+ 配色方案，通过 Preferences \u0026gt; Profiles \u0026gt; Colors \u0026gt; Color Presets 即可切换。\n安装社区主题 # 从 iTerm2-Color-Schemes 下载你喜欢的主题，然后在 Color Presets 中选择 Import 导入。\n我的推荐 # Dracula — 深色主题经典之选，对比度舒适 Solarized Dark — 护眼低对比度，适合长时间工作 One Dark — Atom 编辑器同款，代码高亮清晰 Nord — 冷色调，科技感十足 字体设置 # 好马配好鞍，一个好字体能大幅提升终端体验。推荐 Nerd Font 系列的等宽字体，它们额外集成了大量图标字符，配合 powerlevel10k 等工具显示效果极佳。\n安装推荐字体：\n# FiraCode Nerd Font brew install --cask font-firacode-nerd-font # JetBrains Mono Nerd Font brew install --cask font-jetbrains-mono-nerd-font 然后在 Preferences \u0026gt; Profiles \u0026gt; Text \u0026gt; Font 中选择安装的字体，记得开启 Anti-aliased 和 Use a different font for non-ASCII text。\n小结 # 安装和主题配置完成后，你的 iTerm2 已经告别了默认 Terminal 的朴素外观。下一篇我们开始实际使用，看看 iTerm2 的日常操作技巧。\n","date":"2024-01-14","externalUrl":null,"permalink":"/docs/mytermenv/terminal/install/","section":"教程","summary":"本节介绍 iTerm2 安装与主题。\n安装 # 安装 iTerm2 很简单，直接去官网 iterm2.com 下载安装包，或者用 Homebrew：\n","title":"安装与主题","type":"docs"},{"content":"上节中，我们对 iTerm2 已经有了一个大概认识。但一个高效的终端环境，离不开一个优秀 shell 解释器。本章将主要介绍 zsh 的安装和简单介绍。\n如果你想深入学习 zsh，推荐 awesome-zsh-plugins。或看阅读一个 zsh 深度系列文章：\nA Guide to Zsh Expansion with Examples A Guide to the Zsh Completion with Examples A Guide to the Zsh Line Editor with Examples Configuring Zsh Without Dependencies ","externalUrl":null,"permalink":"/docs/mytermenv/zsh/","section":"教程","summary":"上节中，我们对 iTerm2 已经有了一个大概认识。但一个高效的终端环境，离不开一个优秀 shell 解释器。本章将主要介绍 zsh 的安装和简单介绍。\n如果你想深入学习 zsh，推荐 awesome-zsh-plugins。或看阅读一个 zsh 深度系列文章：\n","title":"zsh","type":"docs"},{"content":"Cerebro 类的详细说明如下。\n实例化参数 # 参数 默认值 说明 preload True 是否预加载传递给策略的数据源。 runonce True 以矢量化模式计算指标，提升系统性能。\n注： 策略和观察者始终基于事件运行。 live False 如果没有数据通过 islive 方法报告为实时，但用户仍希望以实时模式运行，可将此参数设为 True。 maxcpus None 优化时使用的 CPU 核心数，默认 None（启用所有可用核心）。 stdstats True 如果为 True，将添加默认观察者：经纪人（现金和价值）、交易和买卖。 oldbuysell False 当 stdstats 为 True 且自动添加观察者时，此开关控制买卖观察者的具体行为。 oldtrades False 当 stdstats 为 True 且自动添加观察者时，此开关控制交易观察者的具体行为。 exactbars False - False：默认值，将 Line 中存储的值都保存到内存。- True 或 1：所有 Line 对象的内存使用减少至计算所需的最小周期。如果简单移动平均线的周期为 30，则底层数据将始终有一个 30 条的运行缓冲区，以允许计算 SMA。此设置将停用 preload 和 runonce。使用此设置还将停用绘图。 -1：策略级别的数据源和指标/操作将保留所有数据在内存中。如 RSI 通过指标 UpDay 计算，UpDay 不保留所有数据在内存中。这允许保持绘图和预加载功能。runonce 将被停用。-2：作为策略属性的数据源和指标将保留所有点在内存中。例如：RSI 内部使用指标 UpDay 进行计算，此子指标不保留所有数据在内存中。如果在 __init__ 中定义了 a = self.data.close - self.data.high，那么 a 不保留所有数据在内存中。这允许保持绘图和预加载功能。runonce 将被停用。 objcache False 实验选项，用于缓存 Line 对象并减少其数量。示例来自 UltimateOscillator：bp = self.data.close - TrueLow(self.data) # -\u0026gt; 创建另一个 TrueLow(self.data)tr = TrueRange(self.data) 如果为 True，TrueRange 内部的第二个 TrueLow(self.data) 将匹配 bp 计算中的签名并被重用。极端情况下可能使线对象超出其最小周期而导致问题，因此该功能默认禁用。 writer False 如果为 True，将创建一个默认的 WriterFile，输出到标准输出。除用户代码添加的编写器外，这个编写器也会添加到策略中。 tradehistory False 如果为 True，将激活所有策略中每个交易的更新事件日志。也可通过策略方法 set_tradehistory 按策略单独设置。 optdatas True 如果为 True 且在优化时（且系统可预加载并使用 runonce），则数据预加载仅在主进程中完成，以节省时间和资源。测试显示，执行时间从 83 秒减少到 66 秒，提升约 20%。 optreturn True 如果为 True，优化结果不是完整的策略对象（包括所有数据、指标、观察者等），而是只包含以下属性的对象（与策略一致）： params（或 p） 无 策略执行时的参数。 analyzers 无 策略所执行的分析器。多数情况下只需分析器和参数即可评估策略性能。如需详细分析生成的值（如指标值），可关闭此选项。测试显示，执行时间提升了 13%-15%，结合 optdatas 总提升达 32%。 oldsync False 从版本 1.9.0.99 开始，多个数据（相同或不同时间框架）的同步方式已变更，以支持不同长度的数据。如需恢复以数据 0 作为系统主控的旧行为，请将此参数设为 True。 tz None 为策略添加全局时区。tz 可以是：None：策略显示的日期时间将为 UTC，此为标准行为。pytz 实例：用于将 UTC 转换为所选时区。字符串：将尝试实例化 pytz 实例。整数：使用 self.datas 中对应数据的时区（0 表示使用 data0 的时区）。 cheat_on_open False 调用策略的 next_open 方法。该方法在 next 之前触发，且早于经纪人评估订单的时机。此时指标尚未重新计算，允许在开盘价上进行股数计算时参考前一天的指标发布订单。要启用 cheat_on_open 订单执行，还需调用 cerebro.broker.set_coo(True)、实例化 BackBroker(coo=True)（coo 表示 cheat-on-open），或将 broker_coo 参数设为 True。除非在下文中禁用，否则 Cerebro 会自动执行此操作。 broker_coo True 自动调用经纪人的 set_coo 方法并设为 True，以激活 cheat_on_open 执行。仅在 cheat_on_open 也为 True 时生效。 quicknotify False 在传递下一价格之前立即传递经纪人通知。对回测无影响，但对实时经纪人，通知可能在条传递之前很久就已发生。设为 True 时将尽快传递通知（参见实时数据源中的 qcheck）。出于兼容性，默认为 False，未来可能改为 True。 成员方法 # addstorecb(callback) 添加回调函数，接收将由 notify_store 方法处理的消息。回调的签名必须支持以下内容：\ncallback(msg, *args, **kwargs) 实际接收的 msg、*args 和 **kwargs 由具体实现决定（完全取决于数据源/经纪人/存储），但通常这些参数应该是可打印的，便于查看和调试。\nnotify_store(msg, *args, **kwargs) 在 Cerebro 中接收存储通知。此方法可在 Cerebro 子类中覆盖。\nadddatacb(callback) 添加回调函数，接收将由 notify_data 方法处理的消息。回调的签名必须支持以下内容：\ncallback(data, status, *args, **kwargs) 实际接收的 *args 和 **kwargs 由具体实现决定（完全取决于数据源/经纪人/存储），但通常这些参数应该是可打印的，便于查看和调试。\nnotify_data(data, status, *args, **kwargs) 在 Cerebro 中接收数据通知。此方法可在 Cerebro 子类中覆盖。\nadddata(data, name=None) 添加数据源实例。如果 name 不为 None，会将其赋值给 data._name，用于显示和绘图。\nresampledata(dataname, name=None, **kwargs) 添加数据源用于重采样。如果 name 不为 None，会将其赋值给 data._name，用于显示和绘图。其他支持的参数（如 timeframe、compression、todate 等）会透明传递。\nreplaydata(dataname, name=None, **kwargs) 添加数据源用于重放。如果 name 不为 None，会将其赋值给 data._name，用于显示和绘图。其他支持的参数（如 timeframe、compression、todate 等）会透明传递。\nchaindata(*args, **kwargs) 将多个数据源链接成一个。如果通过命名参数传递且 name 不为 None，会将其赋值给 data._name，用于显示和绘图。如果为 None，则使用第一个数据的名称。\nrolloverdata(*args, **kwargs) 将多个数据源链接成一个。如果通过命名参数传递且 name 不为 None，会将其赋值给 data._name，用于显示和绘图。如果为 None，则使用第一个数据的名称。其他 kwargs 会传递给 RollOver 类。\naddstrategy(strategy, *args, **kwargs) 为单次运行添加策略类，在运行时实例化。args 和 kwargs 将按原样传递给策略。返回的索引可用于引用添加的其他对象（如 Sizer）。\noptstrategy(strategy, *args, **kwargs) 为优化添加策略类，在运行时实例化。args 和 kwargs 必须是包含待测试值的可迭代对象。例如，如果策略接受参数 period，可按如下方式调用 optstrategy：\ncerebro.optstrategy(MyStrategy, period=(15, 25)) 这将执行值为15和25的优化。而：\ncerebro.optstrategy(MyStrategy, period=range(15, 25)) 将以 period 值 15 到 25（不含 25，因为 Python 的 range 是半开区间）执行 MyStrategy。如果某个参数只需传递一个值而不需要优化，调用如下：\ncerebro.optstrategy(MyStrategy, period=(15,)) 注意 period 仍然作为只有一个元素的可迭代对象传递。Backtrader 仍会尝试识别以下情况：\ncerebro.optstrategy(MyStrategy, period=15) 如果可行，将创建一个内部的伪可迭代对象。\noptcallback(cb) 添加回调函数，在优化过程中每个策略运行时会触发调用。签名：cb(strategy)。\naddindicator(indcls, *args, **kwargs) 添加指标类到系统中。在策略中进行实例化。\naddobserver(obscls, *args, **kwargs) 添加观察者类到系统中。在运行时进行实例化。\naddobservermulti(obscls, *args, **kwargs) 添加观察者类到系统中。在运行时进行实例化。会为每个数据分别添加一次。例如，观察单个数据的买入/卖出观察者；反之，CashValue 观察的是系统范围的值。\naddanalyzer(ancls, *args, **kwargs) 添加分析器类到系统中。在运行时进行实例化。\naddwriter(wrtcls, *args, **kwargs) 添加编写器类到系统中。在 Cerebro 中进行实例化。\nrun(**kwargs) 执行回测的核心方法。传递的 kwargs 会覆盖 Cerebro 实例化时的标准参数值。如果未添加数据，该方法将立即退出。返回值因是否优化而异：\n未优化： 返回包含通过 addstrategy 添加的策略实例的列表。 优化： 返回列表的列表，每个子列表包含一次优化运行后的策略实例。\nrunstop() 在策略内部或其他地方（包括其他线程）调用时，将尽快停止执行。\nsetbroker(broker) 设置特定的经纪人实例，替换 Cerebro 的默认经纪人。\ngetbroker() 返回经纪人实例。也可通过 broker 属性访问。\nplot(plotter=None, numfigs=1, iplot=True, start=None, end=None, width=16, height=9, dpi=300, tight=True, use=None, **kwargs) 绘制 Cerebro 中的策略。如果 plotter 为 None，会创建一个默认的 Plot 实例，并在实例化时传递 kwargs。\n`numfigs`将图表分成所指示的数量，以减少图表密度。\u0026lt;br/\u0026gt; `iplot`：如果为True且在笔记本中运行，图表将内联显示。\u0026lt;br/\u0026gt; `use`：将其设置为所需matplotlib后端的名称。它将优先于`iplot`。\u0026lt;br/\u0026gt; `start`：策略日期时间线数组的索引或表示绘图开始的`datetime.date`、`datetime.datetime`实例。\u0026lt;br/\u0026gt; `end`：策略日期时间线数组的索引或表示绘图结束的`datetime.date`、`datetime.datetime`实例。\u0026lt;br/\u0026gt; `width`：保存图形的宽度（以英寸为单位）。\u0026lt;br/\u0026gt; `height`：保存图形的高度（以英寸为单位）。\u0026lt;br/\u0026gt; `dpi`：保存图形的质量（以每英寸点数为单位）。\u0026lt;br/\u0026gt; `tight`：仅保存实际内容而不保存图形框架。 addsizer(sizercls, *args, **kwargs) 添加 Sizer 类（及参数），作为添加到 Cerebro 的所有策略的默认 Sizer。\naddsizer_byidx(idx, sizercls, *args, **kwargs) 按 idx 索引添加 Sizer 类。该索引与 addstrategy 返回的索引兼容，只有此索引引用的策略会应用该 Sizer。\nadd_signal(sigtype, sigcls, *sigargs, **sigkwargs) 向系统添加信号，后续会添加到 SignalStrategy 中。\nsignal_concurrent(onoff) 为系统添加信号并将 concurrent 设为 True 后，将允许并发订单。\nsignal_accumulate(onoff) 为系统添加信号并将 accumulate 设为 True 后，当已有持仓时再次入场将增加头寸。\nsignal_strategy(stratcls, *args, **kwargs) 添加支持信号的 SignalStrategy 子类。\naddcalendar(cal) 向系统添加全局交易日历。单个数据源可用独立日历覆盖全局日历。cal 可以是 TradingCalendar 实例、字符串或 pandas_market_calendars 实例。字符串会实例化为 PandasMarketCalendar（需在系统中安装 pandas_market_calendar 模块）。如果传递的是 TradingCalendarBase 的子类（而非实例），也会被实例化。\naddtz(tz) 也可通过参数 tz 完成。为策略添加全局时区，tz 的选项如下：\n可选项 描述 None 策略显示的日期时间将为 UTC，此为标准行为。 pytz 实例 用于将 UTC 时间转换为所选时区。 字符串 将尝试实例化 pytz 实例。 整数 使用 self.datas 中对应数据的时区（0 表示使用 data0 的时区）。 add_timer(when, offset=datetime.timedelta(0), repeat=datetime.timedelta(0), weekdays=[], weekcarry=False, monthdays=[], monthcarry=True, allow=None, tzdata=None, strats=False, cheat=False, *args, **kwargs) 安排计时器调用 notify_timer。参数如下：\n参数 when 可以是 datetime.time 实例（参见下方 tzdata）、bt.timer.SESSION_START（参考交易时段开始）或 bt.timer.SESSION_END（参考交易时段结束）。 offset 必须是 datetime.timedelta 实例，用于偏移 when 的值。与 SESSION_START 和 SESSION_END 配合使用时有实际意义，例如在交易时段开始后 15 分钟触发计时器。 repeat 必须是 datetime.timedelta 实例，表示首次调用后是否在同一交易时段内按指定间隔重复触发。 weekdays 排序后的可迭代对象，包含计时器可触发的天数（ISO 编码，周一为 1，周日为 7）。未指定时，计时器在所有天均有效。 weekcarry 默认 False，如果为 True 但工作日未发生（如交易假期），计时器将在第二天执行（即使跨周）。 monthdays 排序后的可迭代对象，包含每月应执行计时器的日期。例如每月的 15 日。未指定时，计时器在所有天均有效。 monthcarry 默认 True，如果该天未发生（周末、交易假期），计时器将在下一个可用日执行。 allow 默认 None，一个回调函数，接收 datetime.date 实例，如果允许日期触发计时器则返回 True，否则返回 False。 tzdata 可以是 None（默认）、pytz 实例或数据源实例。None：按字面解释 when（即视为 UTC 时间处理）。pytz 实例：when 将被视为所选时区的本地时间。数据源实例：when 将被视为该数据源 tz 参数指定的本地时间。注意，如果 when 是 SESSION_START 或 SESSION_END 且 tzdata 为 None，将使用系统第一个数据源（self.data0）作为参考以确定交易时段。 strats 默认 False，同时也会调用策略的 notify_timer。 cheat 默认 False，如果为 True，将在经纪人有机会评估订单之前触发计时器。这允许在交易时段开始前发出基于开盘价的订单。 *args 传递给 notify_timer 的额外参数。 **kwargs 传递给 notify_timer 的额外关键字参数。返回值：创建的计时器。 notify_timer(timer, when, *args, **kwargs) 接收计时器通知，其中 timer 是由 add_timer 返回的计时器，when 是调用时间。args 和 kwargs 是传递给 add_timer 的附加参数。实际的 when 时间可能略有延迟，因为系统无法提前调用计时器。此值是计时器的设定值，而非系统时间。\nadd_order_history(orders, notify=True) 将订单历史直接添加到经纪人，用于性能评估。\norders：是一个可迭代对象（例如列表、元组、迭代器、生成器），其中的每个元素也是一个可迭代对象，包含以下子元素（支持 2 种格式）：[datetime, size, price] 或 [datetime, size, price, data]。注意，必须按日期时间升序排序（或生成有序元素）。\n具体说明如下：\n参数 描述 datetime Python 的 date/datetime 实例，或格式为 YYYY-MM-DD[THH:MM:SS[.us]] 的字符串（方括号内为可选部分）。 size 整数（正数表示买入，负数表示卖出）。 price 浮点数或整数。 data 如果存在，可取以下值：None：使用第一个数据源作为目标。integer：使用该索引（按在 Cerebro 中的添加顺序）的数据。string：使用该名称的数据，例如 cerebro.adddata(data, name=value) 指定的名称。 notify（默认 True）：如果为 True，系统中的第一个策略将收到根据每个订单信息创建的人工订单通知。\n注意\n这里隐含了需要将数据源添加为订单的目标。这对分析器（如回报跟踪）来说是必要的。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/05-cerebro/02-reference/","section":"教程","summary":"Cerebro 类的详细说明如下。\n实例化参数 # 参数 默认值 说明 preload True 是否预加载传递给策略的数据源。 runonce True 以矢量化模式计算指标，提升系统性能。\n注： 策略和观察者始终基于事件运行。 live False 如果没有数据通过 islive 方法报告为实时，但用户仍希望以实时模式运行，可将此参数设为 True。 maxcpus None 优化时使用的 CPU 核心数，默认 None（启用所有可用核心）。 stdstats True 如果为 True，将添加默认观察者：经纪人（现金和价值）、交易和买卖。 oldbuysell False 当 stdstats 为 True 且自动添加观察者时，此开关控制买卖观察者的具体行为。 oldtrades False 当 stdstats 为 True 且自动添加观察者时，此开关控制交易观察者的具体行为。 exactbars False - False：默认值，将 Line 中存储的值都保存到内存。- True 或 1：所有 Line 对象的内存使用减少至计算所需的最小周期。如果简单移动平均线的周期为 30，则底层数据将始终有一个 30 条的运行缓冲区，以允许计算 SMA。此设置将停用 preload 和 runonce。使用此设置还将停用绘图。 -1：策略级别的数据源和指标/操作将保留所有数据在内存中。如 RSI 通过指标 UpDay 计算，UpDay 不保留所有数据在内存中。这允许保持绘图和预加载功能。runonce 将被停用。-2：作为策略属性的数据源和指标将保留所有点在内存中。例如：RSI 内部使用指标 UpDay 进行计算，此子指标不保留所有数据在内存中。如果在 __init__ 中定义了 a = self.data.close - self.data.high，那么 a 不保留所有数据在内存中。这允许保持绘图和预加载功能。runonce 将被停用。 objcache False 实验选项，用于缓存 Line 对象并减少其数量。示例来自 UltimateOscillator：bp = self.data.close - TrueLow(self.data) # -\u003e 创建另一个 TrueLow(self.data)tr = TrueRange(self.data) 如果为 True，TrueRange 内部的第二个 TrueLow(self.data) 将匹配 bp 计算中的签名并被重用。极端情况下可能使线对象超出其最小周期而导致问题，因此该功能默认禁用。 writer False 如果为 True，将创建一个默认的 WriterFile，输出到标准输出。除用户代码添加的编写器外，这个编写器也会添加到策略中。 tradehistory False 如果为 True，将激活所有策略中每个交易的更新事件日志。也可通过策略方法 set_tradehistory 按策略单独设置。 optdatas True 如果为 True 且在优化时（且系统可预加载并使用 runonce），则数据预加载仅在主进程中完成，以节省时间和资源。测试显示，执行时间从 83 秒减少到 66 秒，提升约 20%。 optreturn True 如果为 True，优化结果不是完整的策略对象（包括所有数据、指标、观察者等），而是只包含以下属性的对象（与策略一致）： params（或 p） 无 策略执行时的参数。 analyzers 无 策略所执行的分析器。多数情况下只需分析器和参数即可评估策略性能。如需详细分析生成的值（如指标值），可关闭此选项。测试显示，执行时间提升了 13%-15%，结合 optdatas 总提升达 32%。 oldsync False 从版本 1.9.0.99 开始，多个数据（相同或不同时间框架）的同步方式已变更，以支持不同长度的数据。如需恢复以数据 0 作为系统主控的旧行为，请将此参数设为 True。 tz None 为策略添加全局时区。tz 可以是：None：策略显示的日期时间将为 UTC，此为标准行为。pytz 实例：用于将 UTC 转换为所选时区。字符串：将尝试实例化 pytz 实例。整数：使用 self.datas 中对应数据的时区（0 表示使用 data0 的时区）。 cheat_on_open False 调用策略的 next_open 方法。该方法在 next 之前触发，且早于经纪人评估订单的时机。此时指标尚未重新计算，允许在开盘价上进行股数计算时参考前一天的指标发布订单。要启用 cheat_on_open 订单执行，还需调用 cerebro.broker.set_coo(True)、实例化 BackBroker(coo=True)（coo 表示 cheat-on-open），或将 broker_coo 参数设为 True。除非在下文中禁用，否则 Cerebro 会自动执行此操作。 broker_coo True 自动调用经纪人的 set_coo 方法并设为 True，以激活 cheat_on_open 执行。仅在 cheat_on_open 也为 True 时生效。 quicknotify False 在传递下一价格之前立即传递经纪人通知。对回测无影响，但对实时经纪人，通知可能在条传递之前很久就已发生。设为 True 时将尽快传递通知（参见实时数据源中的 qcheck）。出于兼容性，默认为 False，未来可能改为 True。 成员方法 # addstorecb(callback) 添加回调函数，接收将由 notify_store 方法处理的消息。回调的签名必须支持以下内容：\n","title":"Cerebro 参数配置说明","type":"docs"},{"content":"Oanda 的集成支持以下功能：\n实时数据馈送 实时交易 要求 # oandapy：安装命令：pip install git+https://github.com/oanda/oandapy.git pytz（可选，不推荐）：由于外汇市场 24x7 全球交易，默认使用 UTC 时间。如有需要，仍可指定输出时区。 示例代码 # 源代码中包含完整示例：\nsamples/oandatest/oandatest.py\nOanda - 存储 # 存储是实时数据馈送和交易支持的核心，提供了 Oanda API 与数据馈送、经纪代理之间的适配层。\n可以通过以下方法获取经纪商实例：\nOandaStore.getbroker(*args, **kwargs) 可以通过以下方法获取数据馈送实例：\nOandaStore.getdata(*args, **kwargs) 许多 **kwargs 是数据馈送的通用参数，如 dataname、fromdate、todate、sessionstart、sessionend、timeframe、compression。\n数据可能提供其他参数。请参阅下面的参考。\n必要参数 # 连接到 Oanda 需要以下参数：\ntoken（默认：无）：API 访问令牌 account（默认：无）：账户 ID 这些由 Oanda 提供。\n连接模拟或真实服务器：\npractice（默认：False）：使用测试环境 账户需要定期检查现金和价值，可通过以下参数控制刷新频率：\naccount_tmout（默认：10.0）：账户价值/现金刷新周期（秒） Oanda 数据馈送 # 实例化数据时，按照 Oanda 指南传递符号。例如，EUR/USD 需要指定为 EUR_USD：\ndata = oandastore.getdata(dataname=\u0026#39;EUR_USD\u0026#39;, ...) 时间管理 # 除非向数据馈送传递了 tz 参数（pytz 兼容对象），否则所有时间输出均为 UTC 格式。\n回填 # backtrader 不会向 Oanda 发出特殊请求。小时间框架下，模拟服务器返回的回填数据长度为 500 根 K 线。\nOandaBroker - 实时交易 # 使用经纪商 # 要使用 OandaBroker，需替换 cerebro 创建的默认模拟经纪商。\n使用存储模型（推荐）：\nimport backtrader as bt cerebro = bt.Cerebro() oandastore = bt.stores.OandaStore() cerebro.broker = oandastore.getbroker() # 或 cerebro.setbroker(...) 经纪商 - 初始头寸 # 经纪商支持一个参数：\nuse_positions（默认：True）：连接到经纪商提供商时使用现有头寸来启动经纪商。 在实例化时设置为False以忽略任何现有头寸。 操作 # 使用方法与回测一致。使用策略中的标准方法：\nbuy sell close cancel 订单执行类型 # Oanda 支持 backtrader 所需的大部分订单执行类型（Close 除外）。支持的订单类型：\nOrder.Market Order.Limit Order.Stop Order.StopLimit（使用 Stop 和 upperBound/lowerBound 价格） Order.StopTrail 通过 takeprofit 和 stoploss 订单成员创建内部模拟订单来支持括号订单。\n订单有效期 # 回测中的有效期概念（valid 参数）在此同样适用。Oanda 订单的有效期转换如下：\nNone -\u0026gt; Good Til Cancelled 未指定有效期，订单有效直至取消。 datetime/date -\u0026gt; Good Til Date timedelta(x) -\u0026gt; Good Til Date（timedelta(x) 不等于 timedelta()） 表示订单有效期为当前时间加上 timedelta(x)。 timedelta() 或 0 -\u0026gt; Session 已指定值（非 None）但为空值，表示当日（当前会话）有效。 通知 # 标准订单状态通过策略的 notify_order 方法通知（如果覆盖）。\nSubmitted - 订单已发送 Accepted - 订单已放置 Rejected - 表示实际拒绝或在创建过程中无其他状态时使用 Partial - 已部分执行 Completed - 订单已完全执行 Canceled（或 Cancelled） Expired - 订单因过期取消 参考 # OandaStore # class backtrader.stores.OandaStore() 控制与 Oanda 连接的单例类。\n参数：\ntoken（默认：无）：API 访问令牌 account（默认：无）：账户 ID practice（默认：False）：使用测试环境 account_tmout（默认：10.0）：账户价值/现金刷新周期（秒） OandaBroker # class backtrader.brokers.OandaBroker(**kwargs) Oanda 的经纪商实现，将 Oanda 的订单/头寸映射到 backtrader 的内部 API。\n参数：\nuse_positions（默认：True）：连接时使用现有头寸初始化经纪商。 设为 False 可忽略任何现有头寸。 OandaData # class backtrader.feeds.OandaData(**kwargs) Oanda 数据馈送。\n参数：\nqcheck（默认：0.5）：无数据时的唤醒间隔（秒），用于重采样/重放和通知传递。 historical（默认：False）：设为 True 时，首次下载数据后停止。 使用 fromdate 和 todate 作为参考。 如果请求时长超过 Oanda 在给定时间框架/压缩下的限制，将自动分段请求。 backfill_start（默认：True）：启动时执行回填，通过单次请求获取最大历史数据。 backfill（默认：True）：断开/重连后执行回填，按缺口时长下载最小数据量。 backfill_from（默认：None）：可用于初始回填的备用数据源。数据源耗尽后，如有需要再从 Oanda 回填。 bidask（默认：True）：历史/回填请求返回买卖价。设为 False 则返回中间价。 useask（默认：False）：使用卖价而非默认的买价。 includeFirst（默认：True）：控制历史/回填请求中第一个 K 线的返回行为。 reconnect（默认：True）：网络断开时自动重连。 reconnections（默认：-1）：重连次数，-1 表示无限重试。 reconntimeout（默认：5.0）：重连间隔（秒）。 此数据馈送支持的时间框架和压缩映射如下（符合 OANDA API 开发者指南）：\n(TimeFrame.Seconds, 5): 'S5' (TimeFrame.Seconds, 10): 'S10' (TimeFrame.Seconds, 15): 'S15' (TimeFrame.Seconds, 30): 'S30' (TimeFrame.Minutes, 1): 'M1' (TimeFrame.Minutes, 2): 'M3' (TimeFrame.Minutes, 3): 'M3' (TimeFrame.Minutes, 4): 'M4' (TimeFrame.Minutes, 5): 'M5' (TimeFrame.Minutes, 10): 'M10' (TimeFrame.Minutes, 15): 'M15' (TimeFrame.Minutes, 30): 'M30' (TimeFrame.Minutes, 60): 'H1' (TimeFrame.Minutes, 120): 'H2' (TimeFrame.Minutes, 180): 'H3' (TimeFrame.Minutes, 240): 'H4' (TimeFrame.Minutes, 360): 'H6'\n(TimeFrame.Minutes, 480): 'H8' (TimeFrame.Days, 1): 'D' (TimeFrame.Weeks, 1): 'W' (TimeFrame.Months, 1): 'M' 其他任何组合将被拒绝。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/15-livetrading/02-qanda-v1-0/","section":"教程","summary":"Oanda 的集成支持以下功能：\n实时数据馈送 实时交易 要求 # oandapy：安装命令：pip install git+https://github.com/oanda/oandapy.git pytz（可选，不推荐）：由于外汇市场 24x7 全球交易，默认使用 UTC 时间。如有需要，仍可指定输出时区。 示例代码 # 源代码中包含完整示例：\n","title":"OANDA 实盘接入指南","type":"docs"},{"content":"在 Ticket #108 中提出了 pyfolio 的集成需求。\n刚开始看教程觉得很难，因为 zipline 和 pyfolio 之间集成得很紧密，但 pyfolio 提供的一些示例测试数据实际上很有用，帮助理解了幕后运行机制，从而实现了集成。\nbacktrader 中大部分功能已经就位：\n分析器基础设施 子分析器 TimeReturn 分析器 只需要一个主 PyFolio 分析器和三个简单的子分析器。再加上 pyfolio 依赖项之一 pandas 的方法。\n最具挑战的部分是”正确获取所有依赖项”。\n更新 pandas 更新 numpy 更新 scikit-learn 更新 seaborn 在类 Unix 环境中，只要有 C 编译器，就只是时间问题。在 Windows 上，即使安装了特定的 Microsoft 编译器（这里是 Python 2.7 的工具链），事情也会失败。不过一个知名的 Windows 软件包网站提供了帮助。如果你需要，可以访问：\nhttp://www.lfd.uci.edu/~gohlke/pythonlibs/\n没有经过测试的集成不算完成，这就是为什么示例代码始终存在的原因。\n没有 PyFolio # 示例使用 random.randint 来决定何时买卖，因此这只是检查事情是否正常运行：\n$ ./pyfoliotest.py --printout --no-pyfolio --plot 输出：\nLen,Datetime,Open,High,Low,Close,Volume,OpenInterest 0001,2005-01-03T23:59:59,38.36,38.90,37.65,38.18,25482800.00,0.00 BUY 1000 @%23.58 0002,2005-01-04T23:59:59,38.45,38.54,36.46,36.58,26625300.00,0.00 BUY 1000 @%36.58 SELL 500 @%22.47 0003,2005-01-05T23:59:59,36.69,36.98,36.06,36.13,18469100.00,0.00 ... SELL 500 @%37.51 0502,2006-12-28T23:59:59,25.62,25.72,25.30,25.36,11908400.00,0.00 0503,2006-12-29T23:59:59,25.42,25.82,25.33,25.54,16297800.00,0.00 SELL 250 @%17.14 SELL 250 @%37.01 有 3 个数据源，多个买卖操作在默认两年的测试期间随机选择和分散。\n一个 PyFolio 运行 # pyfolio 在 Jupyter Notebook 内运行效果很好，包括内联绘图。以下是 notebook 示例代码：\n%matplotlib inline from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import random import backtrader as bt class St(bt.Strategy): params = ( (\u0026#39;printout\u0026#39;, False), (\u0026#39;stake\u0026#39;, 1000), ) def __init__(self): pass def start(self): if self.p.printout: txtfields = list() txtfields.append(\u0026#39;Len\u0026#39;) txtfields.append(\u0026#39;Datetime\u0026#39;) txtfields.append(\u0026#39;Open\u0026#39;) txtfields.append(\u0026#39;High\u0026#39;) txtfields.append(\u0026#39;Low\u0026#39;) txtfields.append(\u0026#39;Close\u0026#39;) txtfields.append(\u0026#39;Volume\u0026#39;) txtfields.append(\u0026#39;OpenInterest\u0026#39;) print(\u0026#39;,\u0026#39;.join(txtfields)) def next(self): if self.p.printout: txtfields = list() txtfields.append(\u0026#39;%04d\u0026#39; % len(self)) txtfields.append(self.data.datetime.datetime(0).isoformat()) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.open[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.high[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.low[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.close[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.volume[0]) txtfields.append(\u0026#39;%.2f\u0026#39; % self.data0.openinterest[0]) print(\u0026#39;,\u0026#39;.join(txtfields)) for data in self.datas: toss = random.randint(1, 10) curpos = self.getposition(data) if curpos.size: if toss \u0026gt; 5: size = curpos.size // 2 self.sell(data=data, size=size) if self.p.printout: print(\u0026#39;SELL {} @%{}\u0026#39;.format(size, data.close[0])) elif toss \u0026lt; 5: self.buy(data=data, size=self.p.stake) if self.p.printout: print(\u0026#39;BUY {} @%{}\u0026#39;.format(self.p.stake, data.close[0])) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() cerebro.broker.set_cash(args.cash) dkwargs = dict() if args.fromdate: fromdate = datetime.datetime.strptime(args.fromdate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;fromdate\u0026#39;] = fromdate if args.todate: todate = datetime.datetime.strptime(args.todate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;todate\u0026#39;] = todate data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **dkwargs) cerebro.adddata(data0, name=\u0026#39;Data0\u0026#39;) data1 = bt.feeds.BacktraderCSVData(dataname=args.data1, **dkwargs) cerebro.adddata(data1, name=\u0026#39;Data1\u0026#39;) data2 = bt.feeds.BacktraderCSVData(dataname=args.data2, **dkwargs) cerebro.adddata(data2, name=\u0026#39;Data2\u0026#39;) cerebro.addstrategy(St, printout=args.printout) if not args.no_pyfolio: cerebro.addanalyzer(bt.analyzers.PyFolio, _name=\u0026#39;pyfolio\u0026#39;) results = cerebro.run() if not args.no_pyfolio: strat = results[0] pyfoliozer = strat.analyzers.getbyname(\u0026#39;pyfolio\u0026#39;) returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items() if args.printout: print(\u0026#39;-- RETURNS\u0026#39;) print(returns) print(\u0026#39;-- POSITIONS\u0026#39;) print(positions) print(\u0026#39;-- TRANSACTIONS\u0026#39;) print(transactions) print(\u0026#39;-- GROSS LEVERAGE\u0026#39;) print(gross_lev) import pyfolio as pf pf.create_full_tear_sheet( returns, positions=positions, transactions=transactions, gross_lev=gross_lev, live_start_date=\u0026#39;2005-05-01\u0026#39;, round_trips=True) if args.plot: cerebro.plot(style=args.plot_style) def parse_args(args=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Sample for pivot point and cross plotting\u0026#39;) parser.add_argument(\u0026#39;--data0\u0026#39;, required=False, default=\u0026#39;../../datas/yhoo-1996-2015.txt\u0026#39;, help=\u0026#39;Data to be read in\u0026#39;) parser.add_argument(\u0026#39;--data1\u0026#39;, required=False, default=\u0026#39;../../datas/orcl-1995-2014.txt\u0026#39;, help=\u0026#39;Data to be read in\u0026#39;) parser.add_argument(\u0026#39;--data2\u0026#39;, required=False, default=\u0026#39;../../datas/nvda-1999-2014.txt\u0026#39;, help=\u0026#39;Data to be read in\u0026#39;) parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;2005-01-01\u0026#39;, help=\u0026#39;Starting date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;2006-12-31\u0026#39;, help=\u0026#39;Ending date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--printout\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Print data lines\u0026#39;)) parser.add_argument(\u0026#39;--cash\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, type=float, default=50000, help=(\u0026#39;Cash to start with\u0026#39;)) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Plot the result\u0026#39;)) parser.add_argument(\u0026#39;--plot-style\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, default=\u0026#39;bar\u0026#39;, choices=[\u0026#39;bar\u0026#39;, \u0026#39;candle\u0026#39;, \u0026#39;line\u0026#39;], help=(\u0026#39;Plot style\u0026#39;)) parser.add_argument(\u0026#39;--no-pyfolio\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Do not do pyfolio things\u0026#39;)) import sys aargs = args if args is not None else sys.argv[1:] return parser.parse_args(aargs) runstrat([]) 整个数据开始日期：2005-01-03\n整个数据结束日期：2006-12-29\n$ ./pyfoliotest.py --help 使用：\nusage: pyfoliotest.py [-h] [--data0 DATA0] [--data1 DATA1] [--data2 DATA2] [--fromdate FROMDATE] [--todate TODATE] [--printout] [--cash CASH] [--plot] [--plot-style { bar,candle,line}] [--no-pyfolio] Sample for pivot point and cross plotting optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to be read in (default: ../../datas/yhoo-1996-2015.txt) --data1 DATA1 Data to be read in (default: ../../datas/orcl-1995-2014.txt) --data2 DATA2 Data to be read in (default: ../../datas/nvda-1999-2014.txt) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: 2005-01-01) --todate TODATE Ending date in YYYY-MM-DD format (default: 2006-12-31) --printout Print data lines (default: False) --cash CASH Cash to start with (default: 50000) --plot Plot the result (default: False) --plot-style {bar,candle,line} Plot style (default: bar) --no-pyfolio Do not do pyfolio things (default: False) ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/12-analyzers/03-pyfolio-integration/","section":"教程","summary":"在 Ticket #108 中提出了 pyfolio 的集成需求。\n刚开始看教程觉得很难，因为 zipline 和 pyfolio 之间集成得很紧密，但 pyfolio 提供的一些示例测试数据实际上很有用，帮助理解了幕后运行机制，从而实现了集成。\n","title":"Pyfolio 深度集成指南","type":"docs"},{"content":" 内置策略参考 # MA_CrossOver # 别名：SMA_CrossOver。这是一个仅做多的策略，基于移动平均线交叉。\n交易规则 # 买入逻辑： 如果无持仓，fast 移动平均线向上穿过 slow 移动平均线。\n卖出逻辑： 有持仓时，fast 移动平均线向下穿过 slow 移动平均线。\n订单类型： 市价单\n参数：\nfast (10)，_movav (\u0026lt;class ‘backtrader.indicators.sma.SMA’\u0026gt;) slow (30)，_movav (\u0026lt;class ‘backtrader.indicators.sma.SMA’\u0026gt;) SignalStrategy # 此策略的子类旨在使用信号自动操作。信号通常是指标，预期输出值为：\n\u0026gt; 0 表示多头指示 \u0026lt; 0 表示空头指示 信号分为两组，共有 5 种类型。\n主要组：\nLONGSHORT：接受来自该信号的多头和空头指示。 LONG： 接受多头指示进行做多。 接受空头指示平仓多头。但： 如果系统中有 LONGEXIT 信号，将用它来平仓多头。 如果有 SHORT 信号且没有 LONGEXIT 信号，它将被用来平仓多头再开空头。 SHORT： 接受空头指示进行做空。 接受多头指示平仓空头。但： 如果系统中有 SHORTEXIT 信号，将用它来平仓空头。 如果有 LONG 信号且没有 SHORTEXIT 信号，它将被用来平仓空头再开多头。 退出组：\n这两个信号旨在覆盖其他信号，并为平仓提供标准。\nLONGEXIT：接受空头指示平仓多头。 SHORTEXIT：接受多头指示平仓空头。 订单发出：\n订单执行类型为市价单，有效期为“直到取消” (Good until Canceled)。 参数：\nsignals (默认值: []): 允许实例化信号并分配到正确类型的列表/元组。 _accumulate (默认值: False): 允许进入市场（多头/空头），即使已经在市场中。 _concurrent (默认值: False): 允许在已有待执行订单时发出新订单。 _data (默认值: None): 如果系统中存在多个数据，目标数据是哪一个。这可以是： None: 将使用系统中的第一个数据。 int: 表示在该位置插入的数据。 str: 创建数据时给定的名称（参数 name），或通过 cerebro.adddata(\u0026hellip;, name=) 添加时给定的名称。 数据实例。 线：\ndatetime 参数：\nsignals ([]) _accumulate (False) _concurrent (False) _data (None) ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/07-strategy/03-strategy-references/","section":"教程","summary":"内置策略参考 # MA_CrossOver # 别名：SMA_CrossOver。这是一个仅做多的策略，基于移动平均线交叉。\n","title":"策略 API 参考文档","type":"docs"},{"content":"策略通常都需要参数，在 backtrader 中，参数可作为类属性声明，通过元组或字典的形式定义。\n元组：\nclass MyStrategy(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 20),) 字典：\nclass MyStrategy(bt.Strategy): params = dict(period=20) 无论使用元组还是字典，声明后均可通过 self.params 或 self.p 访问参数值。\nclass MyStrategy(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 20),) def __init__(self): sma = btind.SimpleMovingAverage(self.data, period=self.p.period) 这个例子中，self.p.period 获取 period 参数的值。\n参数继承 # 在一个类中定义参数后，子类会自动继承。你可以在子类中重写参数的默认值。\nclass BaseStrategy(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 20),) class MyStrategy(BaseStrategy): params = ((\u0026#39;period\u0026#39;, 30),) # 重写父类的 period 参数 使用多重继承时，子类会继承所有父类的参数。如果多个父类定义了同名参数，子类使用继承列表中最后一个类的默认值。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/02-params/","section":"教程","summary":"策略通常都需要参数，在 backtrader 中，参数可作为类属性声明，通过元组或字典的形式定义。\n元组：\nclass MyStrategy(bt.Strategy): params = (('period', 20),) 字典：\nclass MyStrategy(bt.Strategy): params = dict(period=20) 无论使用元组还是字典，声明后均可通过 self.params 或 self.p 访问参数值。\n","title":"策略参数系统详解","type":"docs"},{"content":"在之前的一篇文章中，我们在同一空间上绘制了原始数据和随机修改后的数据，但它们并不在同一轴上。\n回顾该文章的第一张图片：\n我们可以看到：\n图表左右两侧刻度不同。 摆动的红线（随机化数据）在原始数据周围上下波动约 50 点。 视觉上，随机化数据似乎大部分时间在原始数据上方，这只是刻度不同造成的错觉。 1.9.32.116 版本初步支持同轴绘图，但图例标签会重复（仅标签，非数据），容易造成困惑。\n1.9.33.116 版本修复了此问题，支持完整的同轴绘图。使用方式类似于选择绘图数据：\nimport backtrader as bt cerebro = bt.Cerebro() data0 = bt.feeds.MyFavouriteDataFeed(dataname=\u0026#39;futurename\u0026#39;) cerebro.adddata(data0) data1 = bt.feeds.MyFavouriteDataFeed(dataname=\u0026#39;spotname\u0026#39;) data1.compensate(data0) # 告诉系统 data1 的操作影响 data0 data1.plotinfo.plotmaster = data0 data1.plotinfo.sameaxis = True cerebro.adddata(data1) ... cerebro.run() data1 通过 plotinfo 设置：\n与 plotmaster（data0）在同一空间绘图。 启用 sameaxis。 原因是平台无法预先判断各数据的刻度是否兼容，因此默认使用独立刻度。\n之前的示例中，增加了 sameaxis 绘图选项：\n$ ./future-spot.py --sameaxis 结果图表：\n注意：\n右侧只有一个刻度。 随机化数据明显在原始数据周围摆动，符合预期视觉效果。 示例用法 # $ ./future-spot.py --help usage: future-spot.py [-h] [--no-comp] [--sameaxis] Compensation example optional arguments: -h, --help show this help message and exit --no-comp --sameaxis 示例代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import random import backtrader as bt # 修改收盘价的过滤器 def close_changer(data, *args, **kwargs): data.close[0] += 50.0 * random.randint(-1, 1) return False # 流长度不变 # 重写标准标记 class BuySellArrows(bt.observers.BuySell): plotlines = dict(buy=dict(marker=\u0026#39;$\\u21E7$\u0026#39;, markersize=12.0), sell=dict(marker=\u0026#39;$\\u21E9$\u0026#39;, markersize=12.0)) class St(bt.Strategy): def __init__(self): bt.obs.BuySell(self.data0, barplot=True) # 在此处完成 BuySellArrows(self.data1, barplot=True) # 为不同的数据设置不同的标记 def next(self): if not self.position: if random.randint(0, 1): self.buy(data=self.data0) self.entered = len(self) else: # 在市场中 if (len(self) - self.entered) \u0026gt;= 10: self.sell(data=self.data1) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() dataname = \u0026#39;../../datas/2006-day-001.txt\u0026#39; # 数据馈送 data0 = bt.feeds.BacktraderCSVData(dataname=dataname, name=\u0026#39;data0\u0026#39;) cerebro.adddata(data0) data1 = bt.feeds.BacktraderCSVData(dataname=dataname, name=\u0026#39;data1\u0026#39;) data1.addfilter(close_changer) if not args.no_comp: data1.compensate(data0) data1.plotinfo.plotmaster = data0 if args.sameaxis: data1.plotinfo.sameaxis = True cerebro.adddata(data1) cerebro.addstrategy(St) # 示例策略 cerebro.addobserver(bt.obs.Broker) # 通过 stdstats=False 移除 cerebro.addobserver(bt.obs.Trades) # 通过 stdstats=False 移除 cerebro.broker.set_coc(True) cerebro.run(stdstats=False) # 执行 cerebro.plot(volume=False) # 并绘图 def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=(\u0026#39;Compensation example\u0026#39;)) parser.add_argument(\u0026#39;--no-comp\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;) parser.add_argument(\u0026#39;--sameaxis\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/16-plotting/03-plotting-same-axis/","section":"教程","summary":"在之前的一篇文章中，我们在同一空间上绘制了原始数据和随机修改后的数据，但它们并不在同一轴上。\n回顾该文章的第一张图片：\n我们可以看到：\n图表左右两侧刻度不同。 摆动的红线（随机化数据）在原始数据周围上下波动约 50 点。 视觉上，随机化数据似乎大部分时间在原始数据上方，这只是刻度不同造成的错觉。 1.9.32.116 版本初步支持同轴绘图，但图例标签会重复（仅标签，非数据），容易造成困惑。\n","title":"多指标同轴绘图技术","type":"docs"},{"content":"如果数据源的时间框架不同，在 Cerebro 引擎中的长度也不同，指标就会出错。\n示例计算中，data0 有天的时间框架，data1 有月的时间框架：\npivotpoint = btind.PivotPoint(self.data1) sellsignal = self.data0.close \u0026lt; pivotpoint.s1 在这里，当收盘价低于 s1 线（第一个支撑）时，寻求卖出信号。\nPivotPoint 本身工作于较大的时间框架。\n过去，这会导致以下错误：\nreturn self.array[self.idx + ago] IndexError: array index out of range 原因很简单：self.data.close 从一开始就提供值，但 PivotPoint（及 s1 线）要等整月结束后才有值，大约对应 22 个 self.data0.close 值。这 22 个收盘价期间 s1 还没有值，访问底层数组就会失败。\n线条对象支持 (ago) 操作符（Python 中的 __call__ 特殊方法）以提供其延迟版本：\nclose1 = self.data.close(-1) 在这个例子中，对象 close1（通过 [0] 访问时）始终包含由 close 提供的前一个值（-1）。该语法也被扩展用于时间框架混用。让我们重写上面的 pivotpoint 代码：\npivotpoint = btind.PivotPoint(self.data1) sellsignal = self.data0.close \u0026lt; pivotpoint.s1() () 无参数执行（后台传入 None）。实际发生的情况如下：\npivotpoint.s1() 返回一个内部 LinesCoupler 对象，跟随较大时间框架的节奏。该耦合器用实际 s1 的最新值填充自身（默认以 NaN 开始）。\n但为了实现这一魔法，还需要额外的东西。Cerebro 必须这样创建：\ncerebro = bt.Cerebro(runonce=False) 或这样执行：\ncerebro.run(runonce=False) 这种模式下，指标和延迟求值的线条对象逐步执行，而非在紧密循环中执行。这会降低整体速度，但使操作成为可能。\n底部的示例脚本现在可以运行：\n$ ./mixing-timeframes.py 输出：\n0021,0021,0001,2005-01-31,2984.75,2935.96,0.00 0022,0022,0001,2005-02-01,3008.85,2935.96,0.00 ... 0073,0073,0003,2005-04-15,3013.89,3010.76,0.00 0074,0074,0003,2005-04-18,2947.79,3010.76,1.00 ... 在第 74 行，第一次出现 close \u0026lt; s1。\n脚本还提供了额外的可能性：耦合一个指标的所有线条。之前是：\nself.sellsignal = self.data0.close \u0026lt; pp.s1() 替代方法是：\npp1 = pp() self.sellsignal = self.data0.close \u0026lt; pp1.s1 现在整个 PivotPoint 指标已被耦合，并且可以访问其任何线条（即 p、r1、r2、s1、s2）。脚本只对 s1 感兴趣，访问是直接的：\n$ ./mixing-timeframes.py --multi 输出：\n0021,0021,0001,2005-01-31,2984.75,2935.96,0.00 0022,0022,0001,2005-02-01,3008.85,2935.96,0.00 ... 0073,0073,0003,2005-04-15,3013.89,3010.76,0.00 0074,0074,0003,2005-04-18,2947.79,3010.76,1.00 ... 这里没有意外。与之前相同。“耦合”对象甚至可以绘制：\n$ ./mixing-timeframes.py --multi --plot 完整耦合语法 # 对于具有多条线的线条对象（例如 PivotPoint 指标）：\nobj(clockref=None, line=-1) clockref：如果 clockref 为 None，则周围对象（例如策略）将作为参考，以适应较大时间框架（例如：月）到较小/更快的时间框架（例如：日）。如果需要，可以使用另一个参考。\nline：\n如果默认的 -1 被给定，则所有线条都被耦合。 如果是另一个整数（例如 0 或 1），则单条线将被耦合，并通过索引（来自 obj.lines[x]）获取。 如果传递了字符串，则按名称获取线条。 在示例中，可以这样做：\ncoupled_s1 = pp(line=\u0026#39;s1\u0026#39;) 对于只有一条线的线条对象（例如来自 PivotPoint 指标的 s1 线）：\nobj(clockref=None)（见上文的 `clockref`） 结论 # 使用 () 语法可以在指标中混合不同时间框架的数据源，但需注意 cerebro 需要用 runonce=False 创建。\n脚本代码和用法 # 可以在 backtrader 的源代码中找到示例。用法：\n$ ./mixing-timeframes.py --help usage: mixing-timeframes.py [-h] [--data DATA] [--multi] [--plot] Sample for pivot point and cross plotting optional arguments: -h, --help show this help message and exit --data DATA Data to be read in (default: ../../datas/2005-2006-day-001.txt) --multi Couple all lines of the indicator (default: False) --plot Plot the result (default: False) 代码：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind import backtrader.utils.flushfile class St(bt.Strategy): params = dict(multi=True) def __init__(self): self.pp = pp = btind.PivotPoint(self.data1) pp.plotinfo.plot = False # deactivate plotting if self.p.multi: pp1 = pp() # couple the entire indicators self.sellsignal = self.data0.close \u0026lt; pp1.s1 else: self.sellsignal = self.data0.close \u0026lt; pp.s1() def next(self): txt = \u0026#39;,\u0026#39;.join( [\u0026#39;%04d\u0026#39; % len(self), \u0026#39;%04d\u0026#39; % len(self.data0), \u0026#39;%04d\u0026#39; % len(self.data1), self.data.datetime.date(0).isoformat(), \u0026#39;%.2f\u0026#39; % self.data0.close[0], \u0026#39;%.2f\u0026#39; % self.pp.s1[0], \u0026#39;%.2f\u0026#39; % self.sellsignal[0]]) print(txt) def runstrat(): args = parse_args() cerebro = bt.Cerebro() data = btfeeds.BacktraderCSVData(dataname=args.data) cerebro.adddata(data) cerebro.resampledata(data, timeframe=bt.TimeFrame.Months) cerebro.addstrategy(St, multi=args.multi) cerebro.run(stdstats=False, runonce=False) if args.plot: cerebro.plot(style=\u0026#39;bar\u0026#39;) def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Sample for pivot point and cross plotting\u0026#39;) parser.add_argument(\u0026#39;--data\u0026#39;, required=False, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, help=\u0026#39;Data to be read in\u0026#39;) parser.add_argument(\u0026#39;--multi\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;Couple all lines of the indicator\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, action=\u0026#39;store_true \u0026#39;, help=(\u0026#39;Plot the result\u0026#39;)) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() 希望这能帮助你理解如何在 backtrader 中混合不同时间框架的数据！\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/08-indicators/03-timeframe-mixing/","section":"教程","summary":"如果数据源的时间框架不同，在 Cerebro 引擎中的长度也不同，指标就会出错。\n示例计算中，data0 有天的时间框架，data1 有月的时间框架：\npivotpoint = btind.PivotPoint(self.data1) sellsignal = self.data0.close \u003c pivotpoint.s1 在这里，当收盘价低于 s1 线（第一个支撑）时，寻求卖出信号。\n","title":"多周期混合分析技术","type":"docs"},{"content":"当前版本重新设计 CommInfo 对象，核心目标包括：\n保留原始 CommissionInfo 类和行为 为用户轻松创建自定义佣金方案提供支持 将 xx% 格式作为新佣金方案的默认值，而不是 0.xx（这只是个人偏好），同时保持行为可配置 定义佣金方案 # 这需要 1 到 2 个步骤：\n子类化 CommInfoBase # 仅更改默认参数可能就足够了。backtrader 在 backtrader.commissions 模块中已经有一些预定义。期货的行业标准是每份合约固定金额。可以这样定义：\nclass CommInfo_Futures_Fixed(CommInfoBase): params = ( (\u0026#39;stocklike\u0026#39;, False), (\u0026#39;commtype\u0026#39;, CommInfoBase.COMM_FIXED), ) 对于股票和按百分比计算的佣金：\nclass CommInfo_Stocks_Perc(CommInfoBase): params = ( (\u0026#39;stocklike\u0026#39;, True), (\u0026#39;commtype\u0026#39;, CommInfoBase.COMM_PERC), ) 如上所述，百分比的默认值（通过参数 commission 传递）为 xx%。如果需要旧的行为 0.xx，可以这样实现：\nclass CommInfo_Stocks_PercAbs(CommInfoBase): params = ( (\u0026#39;stocklike\u0026#39;, True), (\u0026#39;commtype\u0026#39;, CommInfoBase.COMM_PERC), (\u0026#39;percabs\u0026#39;, True), ) 重写 _getcommission 方法（如有必要） # 定义如下：\ndef _getcommission(self, size, price, pseudoexec): \u0026#39;\u0026#39;\u0026#39;Calculates the commission of an operation at a given price pseudoexec: if True the operation has not yet been executed \u0026#39;\u0026#39;\u0026#39; 下面的实际示例中有更详细的说明。\n如何将其应用于平台 # 有了 CommInfoBase 子类后，需要使用 broker.addcommissioninfo 而不是常用的 broker.setcommission。后者会在内部使用旧版 CommissionInfoObject。\n... comminfo = CommInfo_Stocks_PercAbs(commission=0.005) # 0.5% cerebro.broker.addcommissioninfo(comminfo) addcommissioninfo 方法定义如下：\ndef addcommissioninfo(self, comminfo, name=None): self.comminfo[name] = comminfo 设置 name 后，comminfo 对象将仅适用于具有该名称的资产。默认值 None 表示适用于系统中的所有资产。\n实际示例 # 问题 #45 提出了一种适用于期货的佣金方案，基于百分比，并使用整个”虚拟”合约价值的佣金百分比，即在佣金计算中包括期货乘数。\nimport backtrader as bt class CommInfo_Fut_Perc_Mult(bt.CommInfoBase): params = ( (\u0026#39;stocklike\u0026#39;, False), # Futures (\u0026#39;commtype\u0026#39;, bt.CommInfoBase.COMM_PERC), # Apply % Commission # (\u0026#39;percabs\u0026#39;, False), # pass perc as xx% which is the default ) def _getcommission(self, size, price, pseudoexec): return size * price * self.p.commission * self.p.mult 将其放入系统中：\ncomminfo = CommInfo_Fut_Perc_Mult( commission=0.1, # 0.1% mult=10, margin=2000 # Margin is needed for futures-like instruments ) cerebro.addcommissioninfo(comminfo) 如果更倾向于 0.xx 格式，只需将参数 percabs 设置为 True：\nclass CommInfo_Fut_Perc_Mult(bt.CommInfoBase): params = ( (\u0026#39;stocklike\u0026#39;, False), # Futures (\u0026#39;commtype\u0026#39;, bt.CommInfoBase.COMM_PERC), # Apply % Commission (\u0026#39;percabs\u0026#39;, True), # pass perc as 0.xx ) comminfo = CommInfo_Fut_Perc_Mult( commission=0.001, # 0.1% mult=10, margin=2000 # Margin is needed for futures-like instruments ) cerebro.addcommissioninfo(comminfo) 解释 pseudoexec # 让我们回顾 _getcommission 的定义：\ndef _getcommission(self, size, price, pseudoexec): \u0026#39;\u0026#39;\u0026#39;Calculates the commission of an operation at a given price pseudoexec: if True the operation has not yet been executed \u0026#39;\u0026#39;\u0026#39; pseudoexec 参数用于平台可能调用此方法进行可用现金预计算等任务。\n这意味着该方法可能（实际上也确实会）使用相同参数被多次调用。\npseudoexec 指示调用是否对应订单的实际执行。虽然乍看之下似乎无关紧要，但在以下场景中却很重要：\n经纪商对期货交易量超过 5000 张合约时提供 50% 的佣金折扣。\n在这种情况下，如果没有 pseudoexec，多次非执行调用会错误地触发折扣条件。\n将此情景付诸实践：\nimport backtrader as bt class CommInfo_Fut_Discount(bt.CommInfoBase): params = ( (\u0026#39;stocklike\u0026#39;, False), # Futures (\u0026#39;commtype\u0026#39;, bt.CommInfoBase.COMM_FIXED), # Apply Commission # Custom params for the discount (\u0026#39;discount_volume\u0026#39;, 5000), # minimum contracts to achieve discount (\u0026#39;discount_perc\u0026#39;, 50.0), # 50.0% discount ) negotiated_volume = 0 # attribute to keep track of the actual volume def _getcommission(self, size, price, pseudoexec): if self.negotiated_volume \u0026gt; self.p.discount_volume: actual_discount = self.p.discount_perc / 100.0 else: actual_discount = 0.0 commission = self.p.commission * (1.0 - actual_discount) commvalue = size * price * commission if not pseudoexec: # keep track of actual real executed size for future discounts self.negotiated_volume += size return commvalue CommInfoBase 文档字符串和参数 # 有关 CommInfoBase 的参考，请参见”佣金”一章。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/11-commission-schemes/03-customization/","section":"教程","summary":"当前版本重新设计 CommInfo 对象，核心目标包括：\n保留原始 CommissionInfo 类和行为 为用户轻松创建自定义佣金方案提供支持 将 xx% 格式作为新佣金方案的默认值，而不是 0.xx（这只是个人偏好），同时保持行为可配置 定义佣金方案 # 这需要 1 到 2 个步骤：\n","title":"高级佣金定制指南","type":"docs"},{"content":" Benchmark # class backtrader.observers.Benchmark() 该观察器存储策略的回报以及作为参考资产的回报，参考资产是传递给系统的一个数据源。\n参数：\ntimeframe（默认：无）：如果为 None，则报告整个回测期间的总回报。 compression（默认：无）：仅用于子日时间框架，例如通过指定 “TimeFrame.Minutes” 和 60 作为压缩在每小时时间框架上工作。 data（默认：无）：跟踪的参考资产以便进行比较。 注意：此数据必须已通过 adddata、resampledata 或 replaydata 添加到 cerebro 实例中。\n_doprenext（默认：False）：基准测试将在策略开始运行时进行（即策略的最小周期已达到时）。将其设置为 True 将从数据源的起点记录基准值。 firstopen（默认：False）：保持为 False 确保价值和基准之间的首次比较点从 0% 开始，因为基准不会使用其开盘价。参见 TimeReturn 分析器参考以获得参数的完整解释。 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。设置为 True 或 False 以获得特定行为。 记住，在运行的任何时刻都可以通过查看索引 0 处的线条名称来检查当前值。\nBroker # class backtrader.observers.Broker(*args, **kwargs) 该观察器跟踪经纪商中的当前现金金额和投资组合价值（包括现金）。\n参数：无\nBroker - Cash # class backtrader.observers.Cash(*args, **kwargs) 该观察器跟踪经纪商中的当前现金金额。\n参数：无\nBroker - Value # class backtrader.observers.Value(*args, **kwargs) 该观察器跟踪经纪商中的当前投资组合价值，包括现金。\n参数：\nfund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。设置为 True 或 False 以获得特定行为。 BuySell # class backtrader.observers.BuySell(*args, **kwargs) 该观察器跟踪单个买入/卖出订单（单次执行），并在图表上围绕执行价格水平绘制它们。\n参数：\nbarplot（默认：False）：在最低点下方绘制买入信号，在最高点上方绘制卖出信号。如果为 False，则将在条形的平均执行价格上绘制。 bardist（默认：0.015 1.5%）：当 barplot 为 True 时，与最大值/最小值的距离。 DrawDown # class backtrader.observers.DrawDown() 该观察器跟踪当前的回撤水平（绘图）和最大回撤（不绘图）水平。\n参数：\nfund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。设置为 True 或 False 以获得特定行为。 TimeReturn # class backtrader.observers.TimeReturn() 该观察器存储策略的回报。\n参数：\ntimeframe（默认：无）：如果为 None，则报告整个回测期间的总回报。传递 TimeFrame.NoTimeFrame 以考虑整个数据集而不受时间限制。 compression（默认：无）：仅用于子日时间框架，例如通过指定 “TimeFrame.Minutes” 和 60 作为压缩在每小时时间框架上工作。 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。设置为 True 或 False 以获得特定行为。 记住，在运行的任何时刻都可以通过查看索引 0 处的线条名称来检查当前值。\nTrades # class backtrader.observers.Trades() 该观察器跟踪完整的交易，并在交易关闭时绘制实现的损益。\n交易在头寸从 0（或跨越 0）变为 X 时开立，在回到 0（或反向跨越 0）时关闭。\n参数：\npnlcomm（默认：True）：显示净利润和亏损，即扣除佣金后的结果。如果设置为 False，将显示扣除佣金前的交易结果。 LogReturns # class backtrader.observers.LogReturns() 该观察器存储策略的对数回报。\n参数：\ntimeframe（默认：无）：如果为 None，则报告整个回测期间的总回报。传递 TimeFrame.NoTimeFrame 以考虑整个数据集而不受时间限制。 compression（默认：无）：仅用于子日时间框架，例如通过指定 “TimeFrame.Minutes” 和 60 作为压缩在每小时时间框架上工作。 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。设置为 True 或 False 以获得特定行为。 记住，在运行的任何时刻都可以通过查看索引 0 处的线条名称来检查当前值。\nLogReturns2 # class backtrader.observers.LogReturns2() 扩展 LogReturns 观察器以显示两个工具。\nFundValue # class backtrader.observers.FundValue(*args, **kwargs) 该观察器跟踪当前的基金价值。\n参数：无\nFundShares # class backtrader.observers.FundShares(*args, **kwargs) 该观察器跟踪当前的基金份额。\n参数：无\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/13-observers/03-reference/","section":"教程","summary":"Benchmark # class backtrader.observers.Benchmark() 该观察器存储策略的回报以及作为参考资产的回报，参考资产是传递给系统的一个数据源。\n","title":"观察器 API 参考文档","type":"docs"},{"content":"首先，用两句话总结 backtrader 的工作方式：\n它像一个构建工具包，核心模块（Cerebro）可以插入各种不同模块。\n基础发行版包含指标、分析器、观察器、仓位计算器、过滤器、数据源、经纪商、佣金/资产信息方案等模块。\n可以轻松从头创建新模块，或基于现有模块扩展。\nCerebro 已实现自动”插拔”，使用户无需关注所有细节就能快速上手。\n框架预配置了默认行为，例如：\n使用单一主数据源 1 天时间框架/压缩 10,000 单位货币 股票交易 这些设置不一定适合所有人，但重要的是：一切都可以根据需求定制。\n交易股票：整数\n如上所述，默认配置用于股票交易，买入/卖出的是完整的股票份额（如 1、2、50、1000，而非 1.5 或 1001.7589）。\n在默认配置下执行以下代码时：\ndef next(self): # 将投资组合的 50% 用于购买主资产 self.order_target_percent(target=0.5) 系统会计算所需的股票数量，以使该资产在投资组合中的价值尽可能接近 50%。\n但由于默认配置针对股票交易，最终股票数量为整数。\n注意\n请注意，默认配置是使用单一的主数据源，因此在调用 order_target_percent 时，实际的数据并未指定。当使用多个数据源时，必须指定获取/卖出哪个数据（除非是主数据源）。\n交易加密货币：分数\n显然，加密货币交易可以购买”半个比特币”，哪怕小数点后有 20 位数字。\n好消息是，可以通过 CommissionInfo 可插拔模块更改资产相关信息。\n文档：Docs - Commission Schemes\n注意\n不得不承认，命名不太准确，因为这些方案不仅包含佣金信息。\n在分数方案中，关注的是 getsize(price, cash) 方法，其文档说明为：\n返回在给定价格下执行现金操作所需的仓位大小 这些方案与经纪商紧密相关，可通过经纪商 API 添加到系统中。\n经纪商文档：Docs - Broker\n相关方法：addcommissioninfo(comminfo, name=None)。当 name 为 None 时，方案应用于所有资产；指定名称时仅应用于该资产。\n实现分数方案\n通过扩展基础方案 CommissionInfo 即可轻松实现。\nclass CommInfoFractional(bt.CommissionInfo): def getsize(self, price, cash): \u0026#39;\u0026#39;\u0026#39;返回按价格执行现金操作所需的分数大小\u0026#39;\u0026#39;\u0026#39; return self.p.leverage * (cash / price) 就是这样。通过子类化 CommissionInfo 并编写一行代码，目标就实现了。由于原始方案定义支持杠杆，因此杠杆也会被考虑进计算中，万一加密货币能够使用杠杆购买（默认值为 1.0，即无杠杆）。\n稍后的代码中，方案将按如下方式添加（通过命令行参数控制）：\nif args.fractional: # 如果需要使用分数方案 cerebro.broker.addcommissioninfo(CommInfoFractional()) 即：添加一个子类化方案的实例（注意 () 用于实例化）。如上所述，name 参数未设置，这意味着它将应用于系统中的所有资产。\n测试效果\n以下是一个实现移动平均交叉策略（多空仓位）的完整脚本，可直接在 shell 中使用。默认数据源来自 backtrader 仓库。\n整数模式：没有分数\n$ ./fractional-sizes.py --plot 2005-02-14,3079.93,3083.38,3065.27,3075.76,0.00 2005-02-15,3075.20,3091.64,3071.08,3086.95,0.00 ... 2005-03-21,3052.39,3059.18,3037.80,3038.14,0.00 2005-03-21,Enter Short 2005-03-22,Sell Order Completed - Size: -16 @Price: 3040.55 Value: -48648.80 Comm: 0.00 2005-03-22,Trade Opened - Size -16 @Price 3040.55 2005-03-22,3040.55,3053.18,3021.66,3050.44,0.00 ... 一个大小为 16 单位的空头交易已经开仓。整个日志（因显而易见的原因未显示）包含了许多其他操作，所有交易都采用整数大小。\n没有分数\n分数模式运行\n经过子类化和一行代码的修改，分数的目标就实现了\u0026hellip;\n$ ./fractional-sizes.py --fractional --plot 2005-02-14,3079.93,3083.38,3065.27,3075.76,0.00 2005-02-15,3075.20,3091.64,3071.08,3086.95,0.00 ... 2005-03-21,3052.39,3059.18,3037.80,3038.14,0.00 2005-03-21,Enter Short 2005-03-22,Sell Order Completed - Size: -16.457437774427774 @Price: 3040.55 Value: -50039.66 Comm: 0.00 2005-03-22,Trade Opened - Size -16.457437774427774 @Price 3040.55 2005-03-22,3040.55,3053.18,3021.66,3050.44,0.00 ... 胜利了！空头交易已经开仓，并且这次使用的是大小为 -16.457437774427774 的分数仓位。\n分数\n注意，图表中的最终投资组合价值是不同的，因为实际的交易大小不同。\n结论\n是的，backtrader 完全可以做到。通过可插拔/可扩展的构建工具包方式，用户可以轻松地根据交易者/程序员的具体需求定制行为。\n脚本\n#!/usr/bin/env python # -*- coding: utf-8; py-indent-offset:4 -*- ############################################################################### # Copyright (C) 2019 Daniel Rodriguez - MIT License # - https://opensource.org/licenses/MIT # - https://en.wikipedia.org/wiki/MIT_License ############################################################################### import argparse import logging import sys import backtrader as bt # This defines not only the commission info, but some other aspects # of a given data asset like the \u0026#34;getsize\u0026#34; information from below # params = dict(stocklike=True) # No margin, no multiplier class CommInfoFractional(bt.CommissionInfo): def getsize(self, price, cash): \u0026#39;\u0026#39;\u0026#39;Returns fractional size for cash operation @price\u0026#39;\u0026#39;\u0026#39; return self.p.leverage * (cash / price) class St(bt.Strategy): params = dict( p1=10, p2=30, # periods for crossover ma=bt.ind.SMA, # moving average to use target=0.5, # percentage of value to use ) def __init__(self): ma1, ma2 = [self.p.ma(period=p) for p in (self.p.p1, self.p.p2)] self.cross = bt.ind.CrossOver(ma1, ma2) def next(self): self.logdata() if self.cross \u0026gt; 0: self.loginfo(\u0026#39;Enter Long\u0026#39;) self.order_target_percent(target=self.p.target) elif self.cross \u0026lt; 0: self.loginfo(\u0026#39;Enter Short\u0026#39;) self.order_target_percent(target=-self.p.target) def notify_trade(self, trade): if trade.justopened: self.loginfo(\u0026#39;Trade Opened - Size {} @Price {}\u0026#39;, trade.size, trade.price) elif trade.isclosed: self.loginfo(\u0026#39;Trade Closed - Size {} @Price {} Comm: {:.2f}\u0026#39;, trade.size, trade.price, trade.commission) def logdata(self): if self.position: self.loginfo(\u0026#39;Pos {} Value {:.2f} Cash {:.2f}\u0026#39;, self.position.size, self.position.value, self.broker.cash) def loginfo(self, *args): \u0026#39;\u0026#39;\u0026#39;Logging helper\u0026#39;\u0026#39;\u0026#39; msg = f\u0026#39;{self.datas[0].datetime.datetime(0)} \u0026#39; msg += \u0026#39; \u0026#39;.join([str(arg) for arg in args]) print(msg) def run(): \u0026#39;\u0026#39;\u0026#39;Main execution code\u0026#39;\u0026#39;\u0026#39; parser = argparse.ArgumentParser( description=\u0026#34;Backtrader with fractional order size\u0026#34; ) parser.add_argument( \u0026#39;-p\u0026#39;, \u0026#39;--plot\u0026#39;, action=\u0026#39;store_true\u0026#39;, default=False, help=\u0026#34;Plot results\u0026#34; ) parser.add_argument( \u0026#39;-f\u0026#39;, \u0026#39;--fractional\u0026#39;, action=\u0026#39;store_true\u0026#39;, default=False, help=\u0026#34;Enable fractional order sizes\u0026#34; ) args = parser.parse_args() # Create a Cerebro engine cerebro = bt.Cerebro() # Load data from Yahoo Finance data = bt.feeds.YahooFinanceData(dataname=\u0026#39;AAPL\u0026#39;) # Add the data to the engine cerebro.adddata(data) if args.fractional: # Fractional orders mode cerebro.broker.addcommissioninfo(CommInfoFractional()) cerebro.addstrategy(St) # Set starting cash (USD) cerebro.broker.set_cash(100000.0) # Set commission (a flat fee per order) cerebro.broker.set_commission(commission=0.005) # Set slippage model (default to 0.001) cerebro.broker.set_slippage_perc(0.001) # Set the portfolio value cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name=\u0026#39;sharpe\u0026#39;) # Print out the starting portfolio value print(f\u0026#34;Starting Portfolio Value: {cerebro.broker.getvalue()}\u0026#34;) # Run the strategy cerebro.run() # Print out the final portfolio value print(f\u0026#34;Ending Portfolio Value: {cerebro.broker.getvalue()}\u0026#34;) if args.plot: cerebro.plot() if __name__ == \u0026#39;__main__\u0026#39;: run() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/19-articles/03-fractional-sizes/","section":"教程","summary":"首先，用两句话总结 backtrader 的工作方式：\n它像一个构建工具包，核心模块（Cerebro）可以插入各种不同模块。\n基础发行版包含指标、分析器、观察器、仓位计算器、过滤器、数据源、经纪商、佣金/资产信息方案等模块。\n","title":"加密货币中的分位仓位管理","type":"docs"},{"content":"版本 1.9.42.116 增加了对交易日历的支持，在以下重采样场景中非常有用：\n从每日到每周的重采样：每周 K 线可以与本周最后一根 K 线一起交付。 交易日历能识别下一个交易日，从而提前判断本周的最后一个交易日。 当交易会话结束时间非常规（可通过数据源指定）时，仍可正常进行日内到每日的重采样。 交易日历接口 # 有一个基类 TradingCalendarBase 用作任何交易日历的基类。它定义了两个必须被重写的方法：\nclass TradingCalendarBase(with_metaclass(MetaParams, object)): def _nextday(self, day): \u0026#39;\u0026#39;\u0026#39; 返回在 `day`（datetime/date 实例）之后的下一个交易日（datetime/date 实例）以及 isocalendar 组件。 返回值是一个包含两个组件的元组：(nextday, (y, w, d))，其中 (y, w, d)。 \u0026#39;\u0026#39;\u0026#39; raise NotImplementedError def schedule(self, day): \u0026#39;\u0026#39;\u0026#39; 返回给定日期（datetime/date 实例）的开盘和收盘时间（`datetime.time`）。 \u0026#39;\u0026#39;\u0026#39; raise NotImplementedError 实现 # PandasMarketCalendar # 该实现基于 pandas_market_calendars 包（源自 Quantopian 的初始功能），安装方式如下：\npip install pandas_market_calendars 实现的接口如下：\nclass PandasMarketCalendar(TradingCalendarBase): \u0026#39;\u0026#39;\u0026#39; `pandas_market_calendars` 的交易日历包装器。必须安装 `pandas_market_calendar` 包。 参数： - `calendar` (默认 `None`) 参数 `calendar` 接受以下内容： - 字符串：支持的日历名称，例如 `NYSE`。包装器会尝试获取一个日历实例。 - 日历实例：由 `get_calendar(\u0026#39;NYSE\u0026#39;)` 返回。 - `cachesize` (默认 `365`) 缓存查找提前天数。 参见： - https://github.com/rsheftel/pandas_market_calendars - http://pandas-market-calendars.readthedocs.io/ \u0026#39;\u0026#39;\u0026#39; params = ( (\u0026#39;calendar\u0026#39;, None), # 一个 pandas_market_calendars 实例或交易所名称 (\u0026#39;cachesize\u0026#39;, 365), # 缓存查找提前天数 ) TradingCalendar # 该实现允许通过指定假期、早收天数、非交易日以及开盘/收盘时间来构建日历：\nclass TradingCalendar(TradingCalendarBase): \u0026#39;\u0026#39;\u0026#39; 交易日历的包装器。必须安装 `pandas_market_calendars` 包。 参数： - `open` (默认 `time.min`) 常规开盘时间。 - `close` (默认 `time.max`) 常规收盘时间。 - `holidays` (默认 `[]`) 非交易日列表（`datetime.datetime` 实例）。 - `earlydays` (默认 `[]`) 确定日期和开盘/收盘时间的不符合常规交易时间的天数列表，每个元组包含 (`datetime.datetime`, `datetime.time`, `datetime.time`)。 - `offdays` (默认 `ISOWEEKEND`) 一周中市场不交易的工作日的 ISO 格式列表（周一：1 -\u0026gt; 周日：7）。这通常是周六和周日，因此为默认值。 \u0026#39;\u0026#39;\u0026#39; params = ( (\u0026#39;open\u0026#39;, time.min), (\u0026#39;close\u0026#39;, _time_max), (\u0026#39;holidays\u0026#39;, []), # 非交易日列表（日期） (\u0026#39;earlydays\u0026#39;, []), # 元组列表（日期，开盘时间，收盘时间） (\u0026#39;offdays\u0026#39;, ISOWEEKEND), # 非交易日列表（ISO 工作日） ) 使用模式 # 全局交易日历 # 通过 Cerebro 添加的全局日历将作为所有数据源的默认日历，除非数据源单独指定了日历：\ndef addcalendar(self, cal): \u0026#39;\u0026#39;\u0026#39;向系统添加全局交易日历。个别数据源可以有单独的日历覆盖全局日历。 `cal` 可以是 `TradingCalendar` 的一个实例、一个字符串或一个 `pandas_market_calendars` 的实例。字符串将被实例化为 `PandasMarketCalendar`（需要在系统中安装 `pandas_market_calendar` 模块）。 如果传递的是 `TradingCalendarBase` 的子类（而不是实例），则会被实例化。 \u0026#39;\u0026#39;\u0026#39; 每个数据源 # 通过指定一个 calendar 参数，遵循与上面描述的 addcalendar 相同的约定。例如：\n... data = bt.feeds.YahooFinanceData(dataname=\u0026#39;YHOO\u0026#39;, calendar=\u0026#39;NYSE\u0026#39;, ...) cerebro.adddata(data) ... 示例 # 从每日到每周 # 让我们看一个示例代码的运行结果。2016 年的复活节星期五（2016-03-25）也是纽约证券交易所的假期。如果运行没有交易日历的示例代码，让我们看看该日期前后的情况。\n在这种情况下，重采样是从每日到每周（使用 YHOO 和 2016 年的每日数据）：\n$ ./tcal.py ... Strategy len 56 datetime 2016-03-23 Data0 len 56 datetime 2016-03-23 Data1 len 11 datetime 2016-03-18 Strategy len 57 datetime 2016-03-24 Data0 len 57 datetime 2016-03-24 Data1 len 11 datetime 2016-03-18 Strategy len 58 datetime 2016-03-28 Data0 len 58 datetime 2016-03-28 Data1 len 12 datetime 2016-03-24 ... 在这个输出中，第一个日期是由策略计算的日期。第二个日期是每日数据的日期。\n如预期，周在 2016-03-24（星期四）结束，但由于没有交易日历，重采样代码无法获知这点，在 2016-03-18（前一周）交付了重采样 K 线。当数据推进到 2016-03-28（星期一）时，重采样器才检测到周变化，并在 2016-03-24 交付了新的重采样 K 线。\n如果使用 NYSE 的 PandasMarketCalendar 并添加绘图，再次运行：\n$ ./tcal.py --plot --pandascal NYSE ... Strategy len 56 datetime 2016-03-23 Data0 len 56 datetime 2016-03-23 Data1 len 11 datetime 2016-03-18 Strategy len 57 datetime 2016-03-24 Data0 len 57 datetime 2016-03-24 Data1 len 12 datetime 2016-03-24 Strategy len 58 datetime 2016-03-28 Data0 len 58 datetime 2016-03-28 Data1 len 12 datetime 2016-03-24 ... 有变化！有了日历，重采样器知道周在 2016-03-24 结束，并在同一天交付了对应的每周重采样 K 线。\n绘图结果如下。\nimage\n由于某些信息可能并不总是可用，可以手动编写日历。对于 NYSE 和 2016 年，日历定义如下：\nclass NYSE_2016(bt.TradingCalendar): params = dict( holidays=[ datetime.date(2016, 1, 1), datetime.date(2016, 1, 18), datetime.date(2016, 2, 15), datetime.date(2016, 3, 25), datetime.date(2016, 5, 30), datetime.date(2016, 7, 4), datetime.date(2016, 9, 5), datetime.date(2016, 11, 24), datetime.date(2016, 12, 26), ] ) 复活节星期五（2016-03-25）被列为假期之一。现在运行示例代码：\n$ ./tcal.py --plot --owncal ... Strategy len 56 datetime 2016-03-23 Data0 len 56 datetime 2016-03-23 Data1 len 11 datetime 2016-03-18 Strategy len 57 datetime 2016-03-24 Data0 len 57 datetime 2016-03-24 Data1 len 12 datetime 2016-03-24 Strategy len 58 datetime 2016-03-28 Data0 len 58 datetime 2016-03-28 Data1 len 12 datetime 2016-03-24 ... 使用手动编写的日历定义得到了相同的结果。\n从分钟到每日 # 使用一些私有日内数据，2016-11-25 市场提前收盘（感恩节后第二天，美东时间 13:00 收盘）。使用第二个示例进行测试。\n注意\n源数据来自显示数据，时区为 CET，即使标的资产 YHOO 在美国交易。代码使用 tzinput='CET' 和 tz='US/Eastern' 进行输入转换和输出显示。\n首先，不使用交易日历：\n$ ./tcal-intra.py ... Strategy len 6838 datetime 2016-11-25 18:00:00 Data0 len 6838 datetime 2016-11-25 13:00:00 Data1 len 21 datetime 2016-11-23 16:00:00 Strategy len 6839 datetime 2016-11-25 18:01:00 Data0 len 6839 datetime 2016-11-25 13:01:00 Data1 len 21 datetime 2016-11-23 16:00:00 Strategy len 6840 datetime 2016-11-28 14:31:00 Data0 len 6840 datetime 2016-11-28 09:31:00 Data1 len 22 datetime 2016-11-25 16:00:00 Strategy len 6841 datetime 2016-11-28 14:32:00 Data0 len 6841 datetime 2016-11-28 09:32:00 Data1 len 22 datetime 2016-11-25 16:00:00 ... 如预期，这一天在 13:00 提前结束，但重采样器不知道（官方会话在 16:00 结束），继续交付前一日（2016-11-23）的重采样日 K 线。新的日 K 线直到下一个交易日（2016-11-28）才首次交付，日期标记为 2016-11-25。\n注意\n数据在 13:01 有一个额外的小时 K 线，这可能是收盘后拍卖过程中提供的最终价格。\n可以添加过滤器过滤掉非交易时段的 K 线（过滤器可从交易日历获取时间），但这不是本示例的重点。\n使用 PandasMarketCalendar 再次运行：\n$ ./tcal-intra.py --pandascal NYSE ... Strategy len 6838 datetime 2016-11-25 18:00:00 Data0 len 6838 datetime 2016-11-25 13:00:00 Data1 len 15 datetime 2016-11-25 13:00:00 Strategy len 6839 datetime 2016-11-25 18:01:00 Data0 len 6839 datetime 2016-11-25 13:01:00 Data1 len 15 datetime 2016-11-25 13:00:00 Strategy len 6840 datetime 2016-11-28 14:31:00 Data0 len 6840 datetime 2016-11-28 09:31:00 Data1 len 15 datetime 2016-11-25 13:00:00 Strategy len 6841 datetime 2016-11-28 14:32:00 Data0 len 6841 datetime 2016-11-28 09:32:00 Data1 len 15 datetime 2016-11-25 13:00:00 ... 现在，当日 K 线在 2016-11-25 的 13:00 交付时（忽略 13:01 的 K 线），重采样代码通过交易日历知道这一天结束了。\n让我们添加一个手动编写的定义。与前面的相同，但扩展了一些早市天数：\nclass NYSE_2016(bt.TradingCalendar): params = dict( holidays=[ datetime.date(2016, 1, 1), datetime.date(2016, 1, 18), datetime.date(2016, 2, 15), datetime.date(2016, 3, 25), datetime.date(2016, 5, 30), datetime.date(2016, 7, 4), datetime.date(2016, 9, 5), datetime.date(2016, 11, 24), datetime.date(2016, 12, 26), ], earlydays=[ (datetime.date(2016, 11, 25), datetime.time(9, 30), datetime.time(13, 1)) ], open=datetime.time(9, 30), close=datetime.time(16, 0), ) 运行：\n$ ./tcal-intra.py --owncal ... Strategy len 6838 datetime 2016-11-25 18:00:00 Data0 len 6838 datetime 2016-11-25 13:00:00 Data1 len 16 datetime 2016-11-25 13:01:00 Strategy len 6839 datetime 2016-11-25 18:01:00 Data0 len 6839 datetime 2016-11-25 13:01:00 Data1 len 16 datetime 2016-11-25 13:01:00 Strategy len 6840 datetime 2016-11-28 14:31:00 Data0 len 6840 datetime 2016-11-28 09:31:00 Data1 len 16 datetime 2016-11-25 13:01:00 Strategy len 6841 datetime 2016-11-28 14:32:00 Data0 len 6841 datetime 2016-11-28 09:32:00 Data1 len 16 datetime 2016-11-25 13:01:00 ... 细心的读者会注意到手动编写的定义将 2016-11-25 的结束时间定义为 13:01（使用 datetime.time(13, 1)）。这只是为了展示手动编写的 TradingCalendar 如何帮助适应。\n现在 2016-11-25 的重采样日线条在 13:01 与 1 分钟条一起交付。\n策略的额外功能 # 策略的日期时间始终在不同时区（实际为 UTC）。在 1.9.42.116 版本中，这也可以同步。以下参数已添加到 Cerebro（可在实例化或 cerebro.run 时使用）：\ntz（默认：None）\n设置策略的全局时区。参数 tz 可以是：\nNone：策略显示的日期时间为 UTC（标准行为）。 pytz 实例：将 UTC 时间转换为所选时区。 string：尝试实例化 pytz 实例。 integer：使用 self.datas 中对应数据的时区（0 使用 data0 的时区）。 也可通过 cerebro.addtz 方法添加：\ndef addtz(self, tz): \u0026#39;\u0026#39;\u0026#39; 这也可以通过参数 `tz` 完成。 添加策略的全局时区。参数 `tz` 可以是： - `None`：在这种情况下，策略显示的日期时间将是 UTC，这一直是标准行为。 - `pytz` 实例。它将用于将 UTC 时间转换为所选时区。 - `string`。尝试实例化一个 `pytz` 实例。 - `integer`。使用相应数据在 `self.datas` 可迭代对象中的相同时区（`0` 将使用 `data0` 的时区）。 \u0026#39;\u0026#39;\u0026#39; 重复上一次的日内示例运行，并使用 0 作为 tz（与 data0 的时区同步），输出如下，关注与上面相同的日期和时间：\n$ ./tcal-intra.py --owncal --cerebro tz=0 ... Strategy len 6838 datetime 2016-11-25 13:00:00 Data0 len 6838 datetime 2016-11-25 13:00:00 Data1 len 15 datetime 2016-11-23 16:00:00 Strategy len 6839 datetime 2016-11-25 13:01:00 Data0 len 6839 datetime 2016-11-25 13:01:00 Data1 len 16 datetime 2016-11-25 13:01:00 Strategy len 6840 datetime 2016-11-28 09:31:00 Data0 len 6840 datetime 2016-11-28 09:31:00 Data1 len 16 datetime 2016-11-25 13:01:00 Strategy len 6841 datetime 2016-11-28 09:32:00 Data0 len 6841 datetime 2016-11-28 09:32:00 Data1 len 16 datetime 2016-11-25 13:01:00 ... 时间戳现在与时区对齐。\n示例使用（tcal.py） # $ ./tcal.py --help usage: tcal.py [-h] [--data0 DATA0] [--offline] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] [--pandascal PANDASCAL | --owncal] [--timeframe {Weeks,Months,Years}] Trading Calendar Sample optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: YHOO) --offline Read from disk with same name as ticker (default: False) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: 2016-01-01) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: 2016-12-31) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) --pandascal PANDASCAL Name of trading calendar to use (default: ) --owncal Apply custom NYSE 2016 calendar (default: False) --timeframe {Weeks,Months,Years} Timeframe to resample to (default: Weeks) 示例使用（tcal-intra.py） # $ ./tcal-intra.py --help usage: tcal-intra.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] [--pandascal PANDASCAL | --owncal] [--timeframe {Days}] Trading Calendar Sample optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: yhoo-2016-11.csv) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: 2016-01-01) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: 2016-12-31) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) --pandascal PANDASCAL Name of trading calendar to use (default: ) --owncal Apply custom NYSE 2016 calendar (default: False) --timeframe {Days} Timeframe to resample to (default: Days) 示例代码（tcal.py） # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class NYSE_2016(bt.TradingCalendar): params = dict( holidays=[ datetime.date(2016, 1, 1), datetime.date(2016, 1, 18), datetime.date(2016, 2, 15), datetime.date(2016, 3, 25), datetime.date(2016, 5, 30), datetime.date(2016, 7, 4), datetime.date(2016, 9, 5), datetime.date(2016, 11, 24), datetime.date(2016, 12, 26), ] ) class St(bt.Strategy): params = dict() def __init__(self): pass def start(self): self.t0 = datetime.datetime.utcnow() def stop(self): t1 = datetime.datetime.utcnow() print(\u0026#39;Duration:\u0026#39;, t1 - self.t0) def prenext(self): self.next() def next(self): print(\u0026#39;Strategy len {} datetime {}\u0026#39;.format(len(self), self.datetime.date()), end=\u0026#39; \u0026#39;) print(\u0026#39;Data0 len {} datetime {}\u0026#39;.format(len(self.data0), self.data0.datetime.date()), end=\u0026#39; \u0026#39;) if len(self.data1): print(\u0026#39;Data1 len {} datetime {}\u0026#39;.format(len(self.data1), self.data1.datetime.date())) else: print() def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # 数据源参数 kwargs = dict() # 解析 from/to 日期 dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) YahooData = bt.feeds.YahooFinanceData if args.offline: YahooData = bt.feeds.YahooFinanceCSVData # 切换为从文件读取 # 数据源 data0 = YahooData(dataname=args.data0, **kwargs) cerebro.adddata(data0) d1 = cerebro.resampledata(data0, timeframe=getattr(bt.TimeFrame, args.timeframe)) d1.plotinfo.plotmaster = data0 d1.plotinfo.sameaxis = True if args.pandascal: cerebro.addcalendar(args.pandascal) elif args.owncal: cerebro.addcalendar(NYSE_2016) # 经纪商 cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # 大小调整器 cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # 策略 cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # 执行 cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # 如果请求则绘图 cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;Trading Calendar Sample\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;YHOO\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) parser.add_argument(\u0026#39;--offline\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;Read from disk with same name as ticker\u0026#39;) # 日期默认值 parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;2016-01-01\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;2016-12-31\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) pgroup = parser.add_mutually_exclusive_group(required=False) pgroup.add_argument(\u0026#39;--pandascal\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, default=\u0026#39;\u0026#39;, help=\u0026#39;Name of trading calendar to use\u0026#39;) pgroup.add_argument(\u0026#39;--owncal\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;Apply custom NYSE 2016 calendar\u0026#39;) parser.add_argument(\u0026#39;--timeframe\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, default=\u0026#39;Weeks\u0026#39;, choices=[\u0026#39;Weeks\u0026#39;, \u0026#39;Months\u0026#39;, \u0026#39;Years\u0026#39;], help=\u0026#39;Timeframe to resample to\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() 示例代码（tcal-intra.py） # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class NYSE_2016(bt.TradingCalendar ): params = dict( holidays=[ datetime.date(2016, 1, 1), datetime.date(2016, 1, 18), datetime.date(2016, 2, 15), datetime.date(2016, 3, 25), datetime.date(2016, 5, 30), datetime.date(2016, 7, 4), datetime.date(2016, 9, 5), datetime.date(2016, 11, 24), datetime.date(2016, 12, 26), ], earlydays=[ (datetime.date(2016, 11, 25), datetime.time(9, 30), datetime.time(13, 1)) ], open=datetime.time(9, 30), close=datetime.time(16, 0), ) class St(bt.Strategy): params = dict() def __init__(self): pass def prenext(self): self.next() def next(self): print(\u0026#39;Strategy len {} datetime {}\u0026#39;.format(len(self), self.datetime.datetime()), end=\u0026#39; \u0026#39;) print(\u0026#39;Data0 len {} datetime {}\u0026#39;.format(len(self.data0), self.data0.datetime.datetime()), end=\u0026#39; \u0026#39;) if len(self.data1): print(\u0026#39;Data1 len {} datetime {}\u0026#39;.format(len(self.data1), self.data1.datetime.datetime())) else: print() def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # 数据源参数 # kwargs = dict(tz=\u0026#39;US/Eastern\u0026#39;) # import pytz # tz = tzinput = pytz.timezone(\u0026#39;Europe/Berlin\u0026#39;) tzinput = \u0026#39;Europe/Berlin\u0026#39; # tz = tzinput tz = \u0026#39;US/Eastern\u0026#39; kwargs = dict(tzinput=tzinput, tz=tz) # 解析 from/to 日期 dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # 数据源 data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) d1 = cerebro.resampledata(data0, timeframe=getattr(bt.TimeFrame, args.timeframe)) # d1.plotinfo.plotmaster = data0 # d1.plotinfo.sameaxis = False if args.pandascal: cerebro.addcalendar(args.pandascal) elif args.owncal: cerebro.addcalendar(NYSE_2016()) # 或者 NYSE_2016() 传递一个实例 # 经纪商 cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # 大小调整器 cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # 策略 cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # 执行 cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # 如果请求则绘图 cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;Trading Calendar Sample\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;yhoo-2016-11.csv\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) # 日期默认值 parser.add ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/17-datetime/03-trading-calendars/","section":"教程","summary":"版本 1.9.42.116 增加了对交易日历的支持，在以下重采样场景中非常有用：\n从每日到每周的重采样：每周 K 线可以与本周最后一根 K 线一起交付。 交易日历能识别下一个交易日，从而提前判断本周的最后一个交易日。 当交易会话结束时间非常规（可通过数据源指定）时，仍可正常进行日内到每日的重采样。 交易日历接口 # 有一个基类 TradingCalendarBase 用作任何交易日历的基类。它定义了两个必须被重写的方法：\n","title":"交易日历配置与应用","type":"docs"},{"content":"Backtrader 已经提供了一些通用和特定的 CSV 数据源。\nGenericCSVData VisualChartCSVData YahooFinanceData（用于在线下载） YahooFinanceCSVData（用于已下载的数据） BacktraderCSVData（内部用于测试，但也可以使用） 即使如此，你可能仍需要开发对特定 CSV 数据源的支持。\n实际上，框架的设计让这件事变得很简单。\n步骤 # 从 backtrader.CSVDataBase 继承 根据需要定义任何参数 在 start 方法中进行任何初始化 在 stop 方法中进行任何清理 定义一个 _loadline 方法，其中实际工作发生。此方法接收一个参数：linetokens。 顾名思义，这是根据分隔符（从基类继承）拆分当前行后得到的标记列表。\n如果解析到新数据，则填充相应的行并返回 True。\n如果没有数据可用（解析结束），则返回 False。框架会在没有更多行时自动处理。\n框架已处理的事项：\n打开文件（或接收类似文件的对象） 跳过标题行（如有） 读取行 标记化 预加载支持（一次性将整个数据加载到内存中） 下面使用 BacktraderCSVData 内部 CSV 解析代码的简化版本。这个版本不需要初始化或清理（比如打开和关闭套接字这类操作）。\n注意：\nbacktrader 数据源需要填充以下行业标准字段：\ndatetime open high low close volume openinterest 如果你的策略或数据浏览只需要收盘价，可以忽略其他字段（每次迭代会自动用 float('NaN') 填充）。\n本例仅支持每日格式：\nimport itertools import backtrader as bt class MyCSVData(bt.CSVDataBase): def start(self): # 对于此数据源类型无需做任何操作 pass def stop(self): # 对于此数据源类型无需做任何操作 pass def _loadline(self, linetokens): i = itertools.count(0) dttxt = linetokens[next(i)] # 格式为 YYYY-MM-DD y = int(dttxt[0:4]) m = int(dttxt[5:7]) d = int(dttxt[8:10]) dt = datetime.datetime(y, m, d) dtnum = date2num(dt) self.lines.datetime[0] = dtnum self.lines.open[0] = float(linetokens[next(i)]) self.lines.high[0] = float(linetokens[next(i)]) self.lines.low[0] = float(linetokens[next(i)]) self.lines.close[0] = float(linetokens[next(i)]) self.lines.volume[0] = float(linetokens[next(i)]) self.lines.openinterest[0] = float(linetokens[next(i)]) return True 代码假设所有字段都存在且可转换为浮点数；日期时间采用固定 YYYY-MM-DD 格式，无需使用 strptime 解析。\n通过添加处理空值和日期格式解析的代码，可以满足更复杂的需求。GenericCSVData 正是这样实现的。\n警告 # 通过继承 GenericCSVData，可以支持很多格式。\n让我们添加对 Sierra Chart 每日格式的支持（始终以 CSV 格式存储）。\n定义（通过查看一个 \u0026lsquo;.dly\u0026rsquo; 数据文件）：\n字段：Date, Open, High, Low, Close, Volume, OpenInterest\n行业标准字段以及 GenericCSVData 已支持的字段，顺序相同（也是行业标准）\n分隔符：,\n日期格式：YYYY/MM/DD\n一个用于这些文件的解析器：\nclass SierraChartCSVData(backtrader.feeds.GenericCSVData): params = ((\u0026#39;dtformat\u0026#39;, \u0026#39;%Y/%m/%d\u0026#39;),) 这里只是重新定义了基类中的一个现有参数，只需要更改日期格式字符串。\nSierra Chart 的解析器就完成了。\n下面是 GenericCSVData 的参数定义作为提醒：\nclass GenericCSVData(feed.CSVDataBase): params = ( (\u0026#39;nullvalue\u0026#39;, float(\u0026#39;NaN\u0026#39;)), (\u0026#39;dtformat\u0026#39;, \u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;), (\u0026#39;tmformat\u0026#39;, \u0026#39;%H:%M:%S\u0026#39;), (\u0026#39;datetime\u0026#39;, 0), (\u0026#39;time\u0026#39;, -1), (\u0026#39;open\u0026#39;, 1), (\u0026#39;high\u0026#39;, 2), (\u0026#39;low\u0026#39;, 3), (\u0026#39;close\u0026#39;, 4), (\u0026#39;volume\u0026#39;, 5), (\u0026#39;openinterest\u0026#39;, 6), ) ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/03-datafeed-development-csv/","section":"教程","summary":"Backtrader 已经提供了一些通用和特定的 CSV 数据源。\nGenericCSVData VisualChartCSVData YahooFinanceData（用于在线下载） YahooFinanceCSVData（用于已下载的数据） BacktraderCSVData（内部用于测试，但也可以使用） 即使如此，你可能仍需要开发对特定 CSV 数据源的支持。\n","title":"开发 CSV 格式数据源","type":"docs"},{"content":"版本 1.9.44.116 添加了 Cheat-On-Open 支持。满足了在收盘价计算后全仓操作的用户需求，他们希望订单与开盘价匹配。\n当开盘价跳空（方向取决于买入或卖出）且现金不足时，全仓操作会失败，导致 Broker 拒绝订单。\n虽然可以通过正向索引 [1] 预知未来价格，但这需要预加载数据，并非总是可行。\n模式 # cerebro = bt.Cerebro(cheat_on_open=True) 这会在系统中激活一个额外的循环，调用策略中的 next_open、nextstart_open 和 prenext_open 方法。\n为了清晰分离常规方法（基于已不可用的价格且未来未知）和作弊模式的操作，增加了一组额外的方法。\n这也避免了对常规 next 方法的重复调用。\n在 xxx_open 方法内部，以下情况保持不变：\n指标尚未重新计算，保持上一个常规 xxx 方法中最后看到的值。 Broker 尚未评估新循环中的待处理订单，可引入新订单并尝试执行。 注意：\nCerebro 还有一个 broker_coo 参数（默认值：True），如果启用了 cheat-on-open，Cerebro 也会尝试在 Broker 中激活它。 Broker 模拟器有 coo 参数和 set_coo 方法来控制此行为。 尝试 Cheat-on-open # 下面的示例策略具有两种行为：\ncheat-on-open 为 True 时，仅从 next_open 操作。 cheat-on-open 为 False 时，仅从 next 操作。 两种情况下，匹配价格相同：\n不启用作弊：订单在前一天收盘后发出，与下一个开盘价匹配。 启用作弊：订单在同一天开盘前发出并执行，也与下一个开盘价匹配。 第二种情况下，可以直接访问当前开盘价，从而精确计算全仓操作的份额。\n两种情况下，next 中都会打印当前的开盘价和收盘价。\n常规执行 # $ ./cheat-on-open.py --cerebro cheat_on_open=False ... 2005-04-07 next, open 3073.4 close 3090.72 2005-04-08 next, open 3092.07 close 3088.92 Strat Len 68 2005-04-08 Send Buy, fromopen False, close 3088.92 2005-04-11 Buy Executed at price 3088.47 2005-04-11 next, open 3088.47 close 3080.6 2005-04-12 next, open 3080.42 close 3065.18 ... 订单：\n2005-04-08 收盘后发出。 2005-04-11 以开盘价 3088.47 执行。 作弊执行 # $ ./cheat-on-open.py --cerebro cheat_on_open=True ... 2005-04-07 next, open 3073.4 close 3090.72 2005-04-08 next, open 3092.07 close 3088.92 2005-04-11 Send Buy, fromopen True, close 3080.6 2005-04-11 Buy Executed at price 3088.47 2005-04-11 next, open 3088.47 close 3080.6 2005-04-12 next, open 3080.42 close 3065.18 ... 订单：\n2005-04-11 开盘前发出。 2005-04-11 以开盘价 3088.47 执行。 总结 # 开盘作弊允许在开盘前发出订单，从而精确计算全仓场景的份额。\n示例用法 # $ ./cheat-on-open.py --help usage: cheat-on-open.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] Cheat-On-Open Sample optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) 示例代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class St(bt.Strategy): params = dict( periods=[10, 30], matype=bt.ind.SMA, ) def __init__(self): self.cheating = self.cerebro.p.cheat_on_open mas = [self.p.matype(period=x) for x in self.p.periods] self.signal = bt.ind.CrossOver(*mas) self.order = None def notify_order(self, order): if order.status != order.Completed: return self.order = None print(\u0026#39;{} {} Executed at price {}\u0026#39;.format( bt.num2date(order.executed.dt).date(), \u0026#39;Buy\u0026#39; * order.isbuy() or \u0026#39;Sell\u0026#39;, order.executed.price) ) def operate(self, fromopen): if self.order is not None: return if self.position: if self.signal \u0026lt; 0: self.order = self.close() elif self.signal \u0026gt; 0: print(\u0026#39;{} Send Buy, fromopen {}, close {}\u0026#39;.format( self.data.datetime.date(), fromopen, self.data.close[0]) ) self.order = self.buy() def next(self): print(\u0026#39;{} next, open {} close {}\u0026#39;.format( self.data.datetime.date(), self.data.open[0], self.data.close[0]) ) if self.cheating: return self.operate(fromopen=False) def next_open(self): if not self.cheating: return self.operate(fromopen=True) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # Data feed kwargs kwargs = dict() # Parse from/to-date dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # Data feed data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) # Broker cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # Sizer cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # Strategy cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # Execute cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # Plot if requested to cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;Cheat-On-Open Sample\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) # Defaults for dates parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False , default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/10-broker/03-cheat-on-open/","section":"教程","summary":"版本 1.9.44.116 添加了 Cheat-On-Open 支持。满足了在收盘价计算后全仓操作的用户需求，他们希望订单与开盘价匹配。\n当开盘价跳空（方向取决于买入或卖出）且现金不足时，全仓操作会失败，导致 Broker 拒绝订单。\n","title":"开盘价作弊模式详解","type":"docs"},{"content":" 1.8.10.96 版本之前，buy 和 sell 通过 sizer 确定持仓大小。但 sizer 无法决定操作方向（买入还是卖出），因此需要一个新的概念来加入智能决策层。这就是策略中的 order_target_xxx 方法家族。\n受 zipline 启发，这些方法允许直接指定最终目标，目标类型包括：\nsize -\u0026gt; 特定资产的股份或合约数量 value -\u0026gt; 资产在投资组合中的货币价值 percent -\u0026gt; 资产占当前投资组合的百分比 注意：这些方法的详细说明可在 Strategy 类的参考文档中找到。它们使用与 buy 和 sell 相同的参数签名，只是将 size 替换为目标参数。\n这些方法的核心是：指定最终目标，由方法自动决定买入或卖出。三种方法的逻辑相同。以下是 order_target_size 的工作方式：\n如果目标大于当前仓位，则发出买入指令，买入的数量为target - position_size。例如：\n仓位：0，目标：7 -\u0026gt; buy(size=7 - 0) -\u0026gt; buy(size=7) 仓位：3，目标：7 -\u0026gt; buy(size=7 - 3) -\u0026gt; buy(size=4) 仓位：-3，目标：7 -\u0026gt; buy(size=7 - -3) -\u0026gt; buy(size=10) 仓位：-3，目标：-2 -\u0026gt; buy(size=-2 - -3) -\u0026gt; buy(size=1) 如果目标小于当前仓位，则发出卖出指令，卖出的数量为position_size - target。例如：\n仓位：0，目标：-7 -\u0026gt; sell(size=0 - -7) -\u0026gt; sell(size=7) 仓位：3，目标：-7 -\u0026gt; sell(size=3 - -7) -\u0026gt; sell(size=10) 仓位：-3，目标：-7 -\u0026gt; sell(size=-3 - -7) -\u0026gt; sell(size=4) 仓位：3，目标：2 -\u0026gt; sell(size=3 - 2) -\u0026gt; sell(size=1) 当使用order_target_value设定目标值时，会考虑资产在投资组合中的当前价值和持仓量，以决定最终的操作。逻辑如下：\n如果目标值大于当前值且仓位\u0026gt;=0 -\u0026gt; 买入 如果目标值大于当前值且仓位\u0026lt;0 -\u0026gt; 卖出 如果目标值小于当前值且仓位\u0026gt;=0 -\u0026gt; 卖出 如果目标值小于当前值且仓位\u0026lt;0 -\u0026gt; 买入 order_target_percent 的逻辑与 order_target_value 相同。该方法简单地根据当前投资组合的总价值确定资产的目标价值。\n示例 # Backtrader 为每个新功能提供示例，本例也不例外。示例在 order_target 子目录中，逻辑简单，用于验证结果是否符合预期：\n奇数月（1月、3月等），使用日期作为目标（order_target_value 将日期乘以1000）； 偶数月（2月、4月等），使用31减去日期作为目标； order_target_size # 我们来看1月和2月的情况。\n$ ./order_target.py --target-size --plot 0001 - 2005-01-03 - Position Size: 00 - Value 1000000.00 0001 - 2005-01-03 - Order Target Size: 03 0002 - 2005-01-04 - Position Size: 03 - Value 999994.39 0002 - 2005-01-04 - Order Target Size: 04 0003 - 2005-01-05 - Position Size: 04 - Value 999992.48 0003 - 2005-01-05 - Order Target Size: 05 0004 - 2005-01-06 - Position Size: 05 - Value 999988.79 ... 0020 - 2005-01-31 - Position Size: 28 - Value 999968.70 0020 - 2005-01-31 - Order Target Size: 31 0021 - 2005-02-01 - Position Size: 31 - Value 999954.68 0021 - 2005-02-01 - Order Target Size: 30 0022 - 2005-02-02 - Position Size: 30 - Value 999979.65 0022 - 2005-02-02 - Order Target Size: 29 0023 - 2005-02-03 - Position Size: 29 - Value 999966.33 0023 - 2005-02-03 - Order Target Size: 28 ... 1月时，目标从年初第一个交易日的3开始递增，持仓从0增加到3，之后每次增加1。\n1月结束时最后一个目标是31。进入2月后，持仓被报告，新目标变为30，然后每次递减1。\norder_target_value # 期望有类似的行为：\n$ ./order_target.py --target-value --plot 0001 - 2005-01-03 - Position Size: 00 - Value 1000000.00 0001 - 2005-01-03 - data value 0.00 0001 - 2005-01-03 - Order Target Value: 3000.00 0002 - 2005-01-04 - Position Size: 78 - Value 999854.14 0002 - 2005-01-04 - data value 2853.24 0002 - 2005-01-04 - Order Target Value: 4000.00 0003 - 2005-01-05 - Position Size: 109 - Value 999801.68 0003 - 2005-01-05 - data value 3938.17 0003 - 2005-01-05 - Order Target Value: 5000.00 0004 - 2005-01-06 - Position Size: 138 - Value 999699.57 ... 0020 - 2005-01-31 - Position Size: 808 - Value 999206.37 0020 - 2005-01-31 - data value 28449.68 0020 - 2005-01-31 - Order Target Value: 31000.00 0021 - 2005-02-01 - Position Size: 880 - Value 998807.33 0021 - 2005-02-01 - data value 30580.00 0021 - 2005-02-01 - Order Target Value: 30000.00 0022 - 2005-02-02 - Position Size: 864 - Value 999510.21 0022 - 2005-02-02 - data value 30706.56 0022 - 2005-02-02 - Order Target Value: 29000.00 0023 - 2005-02-03 - Position Size: 816 - Value 999130.05 0023 - 2005-02-03 - data value 28633.44 0023 - 2005-02-03 - Order Target Value: 28000.00 ... order_target_percent # 此方法仅计算当前投资组合价值的百分比：\n$ ./order_target.py --target-percent --plot 0001 - 2005-01-03 - Position Size: 00 - Value 1000000.00 0001 - 2005-01-03 - data percent 0.00 0001 - 2005-01-03 - Order Target Percent: 0.03 0002 - 2005-01-04 - Position Size: 785 - Value 998532.05 0002 - 2005-01-04 - data percent 0.03 0002 - 2005-01-04 - Order Target Percent: 0.04 0003 - 2005-01-05 - Position Size: 1091 - Value 998007.44 0003 - 2005-01-05 - data percent 0.04 0003 - 2005-01-05 - Order Target Percent: 0.05 0004 - 2005-01-06 - Position Size: 1381 - Value 996985.64 ... 0020 - 2005-01-31 - Position Size: 7985 - Value 991966.28 0020 - 2005-01-31 - data percent 0.28 0020 - 2005-01-31 - Order Target Percent: 0.31 0021 - 2005-02-01 - Position Size: 8733 - Value 988008.94 0021 - 2005-02-01 - data percent 0.31 0021 - 2005-02-01 - Order Target Percent: 0.30 0022 - 2005-02-02 - Position Size: 8530 - Value 995005.45 0022 - 2005-02-02 - data percent 0.30 0022 - 2005-02-02 - Order Target Percent: 0.29 0023 - 2005-02-03 - Position Size: 8120 - Value 991240.75 0023 - 2005-02-03 - data percent 0.29 0023 - 2005-02-03 - Order Target Percent: 0.28 ... 示例使用 # $ ./order_target.py --help usage: order_target.py [-h] [--data DATA] [--fromdate FROMDATE] [--todate TODATE] [--cash CASH] (--target-size | --target-value | --target-percent) [--plot [kwargs]] Sample for Order Target optional arguments: -h, --help show this help message and exit --data DATA Specific data to be read in (default: ../../datas/yhoo-1996-2015.txt) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: 2005-01-01) --todate TODATE Ending date in YYYY-MM-DD format (default: 2006-12-31) --cash CASH Ending date in YYYY-MM-DD format (default: 1000000) --target-size Use order_target_size (default: False) --target-value Use order_target_value (default: False) --target-percent Use order_target_percent (default: False) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example: --plot style=\u0026#34;candle\u0026#34; (to plot candles) (default: None) test\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/09-orders/03-target-orders/","section":"教程","summary":" 1.8.10.96 版本之前，buy 和 sell 通过 sizer 确定持仓大小。但 sizer 无法决定操作方向（买入还是卖出），因此需要一个新的概念来加入智能决策层。这就是策略中的 order_target_xxx 方法家族。\n","title":"目标订单与仓位管理","type":"docs"},{"content":" 本指南将介绍 Backtrader 量化回测框架中的基本配置方法，包括设置初始账户资金、配置交易佣金等核心参数。这些基础配置是进行准确回测的前提，能够帮助你模拟真实的交易环境。\n设置初始账户资金 # 上节中，账户资金使用的是默认值 10,000 货币单位。这个默认值可以通过 cerebro.broker.setcash 方法更改。\ncerebro.broker.setcash(100000.0) 完整示例 # import backtrader as bt def main(): cerebro = bt.Cerebro() cerebro.broker.setcash(100000.0) print(f\u0026#39;初始资金: {cerebro.broker.getvalue()}\u0026#39;) cerebro.run() print(f\u0026#39;最终资金: {cerebro.broker.getvalue()}\u0026#39;) if __name__ == \u0026#39;__main__\u0026#39;: main() 输出：\n初始资金: 100000.00 最终资金: 100000.00 设置交易费率（佣金） # 在回测中，交易成本是影响最终收益的重要因素。Backtrader 允许你灵活配置各种佣金方案。\n按比例设置佣金 # 对于股票等按交易金额比例收取佣金的品种，可以使用 setcommission 方法：\nimport backtrader as bt def main(): cerebro = bt.Cerebro() cerebro.broker.setcash(100000.0) # 设置交易佣金为千分之三（0.3%） cerebro.broker.setcommission(commission=0.003) print(f\u0026#39;初始资金: {cerebro.broker.getvalue():.2f}\u0026#39;) cerebro.run() print(f\u0026#39;最终资金: {cerebro.broker.getvalue():.2f}\u0026#39;) if __name__ == \u0026#39;__main__\u0026#39;: main() 佣金计算说明 # commission=0.003 表示每笔交易收取交易金额的 0.3% 作为佣金 买入和卖出时都会按比例收取 佣金在交易执行时自动从账户中扣除 在实际回测中，佣金会直接影响最终的账户价值。\n设置滑点规则 # Backtrader 也提供了简单的滑点模拟功能。\n最常用的是按比例设置，使用 set_slippage_perc 方法。设置 0.1% 的滑点：\ncerebro.broker.set_slippage_perc(0.001) 计算规则如下：\n买入订单：实际成交价比预期价格高，公式：预期价格 ×（1 + 滑点比例） 卖出订单：实际成交价比预期价格低，公式：预期价格 ×（1 - 滑点比例） 例如市价买入，预期价格 1000，滑点 0.1%，则实际买入价为 1001。\nBacktrader 也支持固定滑点，通过 cerebro.broker.set_slippage_fixed 方法设置。\n如设置固定滑点为 1：\ncerebro.broker.set_slippage_fixed(1) 市价买入时，价格 1000 则实际买入价为 1001；价格 2000 则实际买入价为 2001。\n小结 # 本文介绍了账户资金、佣金和滑点的配置方法。下一节正式开始介绍数据配置。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/02-cash/","section":"教程","summary":" 本指南将介绍 Backtrader 量化回测框架中的基本配置方法，包括设置初始账户资金、配置交易佣金等核心参数。这些基础配置是进行准确回测的前提，能够帮助你模拟真实的交易环境。\n","title":"账户配置与初始资金","type":"docs"},{"content":"在设置过程中或编译第一个应用程序时，可能会遇到一些意外情况。我们在这里尝试解决这些问题。记住，你也可以使用 Fyne Setup 工具检查你的配置。\n编译器问题 # 问：命令未找到：fyne\n答： 如果你已经使用 go install fyne.io/fyne/v2/cmd/fyne@latest 安装了 Fyne 命令，但尝试运行时看到错误，则最可能的问题是你的 Go 安装没有正确设置你的 PATH 环境变量。\nGo 会将工具安装到用户的 GOPATH 位置的 bin 目录中（通常是 ~/go）- 你可以通过检查你的 PATH 变量是否包含此位置来验证这一点。如果似乎缺少这个位置，那么你应该更新你的 PATH 环境变量以包含 ~/go/bin，或者如果你更改了安装位置，则可以使用 $(go env GOPATH)/bin 代替。\n问：构建约束排除了\u0026hellip;中的所有 Go 文件\n答： 如果你正在交叉编译，你可能会看到关于 go 文件被排除的错误，随后是构建失败。当进行标准的 Go 交叉编译时，它会自动关闭 CGo。为了解决这个问题，请确保在你的编译命令中设置 CGO_ENABLED=1。\n问：cc1.exe: 抱歉，未实现：未在 64 位模式中编译\n答： Windows 编译有时会抱怨没有可用的 64 位模式。这通常是因为安装了错误的编译器，或者配置不正确。如果你已经按照我们的 MSYS2 和 MingW64 的 安装说明，那么请确保你使用的是开始菜单中标题为“MSYS2 MinGW 64-bit”的启动器。\n分发 # 问：Apple macOS 说我的应用在下载时已损坏\n答： 当文件在 macOS 计算机上下载时，它们会被标记有“隔离”标志，以便由操作系统检查是否有问题。如果你的应用程序使用从 Apple 购买的证书签名，这不是问题。然而，如果你想在没有那个成本的情况下分享你的软件，这个错误可能会出现 - 并且在 M1/2 计算机上，不可能使用系统设置来允许应用运行。\n修复的方法是删除隔离标志，你可以通过打开终端并执行以下命令来做到这一点：\nsudo xattr -r -d com.apple.quarantine MyApp.app ","date":"2025-06-01","externalUrl":null,"permalink":"/docs/gofyne/10-faq/03-troubleshoot/","section":"教程","summary":"在设置过程中或编译第一个应用程序时，可能会遇到一些意外情况。我们在这里尝试解决这些问题。记住，你也可以使用 Fyne Setup 工具检查你的配置。\n编译器问题 # 问：命令未找到：fyne\n","title":"故障排查","type":"docs"},{"content":"Fyne 工具包中的控件旨在提供干净愉快的用户交互体验，遵循标准主题，并支持快速应用开发、稳定测试和易于维护。这里有一些促进这一目标的设计考虑，我们将在本页探讨它们。\n行为 API # 你会注意到标准控件的 API 全部关于行为和状态——但实际上很少控制元素的外观。这是有意为之的。它使我们的代码，以及应用开发者的代码，能够专注于控件的行为，以便将其渲染过程留给其他代码处理。这使得测试变得更加容易，实际上完整的应用可以在内存中通过单元测试运行，而无需渲染应用。\n你可以扩展现有控件来添加新的行为，而无需担心其渲染方式。也可以编写自己的组件，应用不限于使用提供的控件集。构建自己的控件时，你会注意到渲染细节与状态完全分离——这是上述设计的一部分。WidgetRenderer（渲染Widget的代码）通常持有对它将要渲染的控件的引用，以访问状态或其他信息。当控件状态改变时，将调用 Refresh()——然后会要求渲染器刷新，并且它应该更新显示以反映新状态。建议自定义控件使用当前的Theme，但在看似合适的情况下可以选择指定自己的尺寸、颜色和图标。\n内容填充 # 标准控件使用主题指定的填充来在其图形组件周围创建适当的空间。widget 包使用标准高度和基线来确保提供的布局默认情况下能够很好地对齐。如果你正在构建自定义控件，建议遵循这些指南。\ntheme.Padding() 的值用于布局中以分隔容器的元素，它在应用的各个部分周围创建一致的空间。然而，有些控件的内容应该从范围的边缘内缩。考虑Entry，它有一个背景和边框延伸到边缘，但其内容应该内缩。因此，我们标准化了用于内缩的空间量，以便对齐匹配。\n控件的标准内缩或内容填充被定义为theme.InnerPadding()。标准的填充值是4，内部填充是8。你可以在Label和Entry中看到（文本）内容被这么多内缩，以便它们在并排放置时能够水平和垂直对齐。\n建议自定义控件包含类似的尺寸，以便它们能够与标准控件很好地配合。\n","date":"2025-05-17","externalUrl":null,"permalink":"/docs/gofyne/09-architecture/03-widgets/","section":"教程","summary":"Fyne 工具包中的控件旨在提供干净愉快的用户交互体验，遵循标准主题，并支持快速应用开发、稳定测试和易于维护。这里有一些促进这一目标的设计考虑，我们将在本页探讨它们。\n","title":"控件 Widget","type":"docs"},{"content":" 资源包 # 基于 Go 的应用程序通常构建为单个二进制可执行文件，Fyne应用程序也是如此。单个文件使我们更容易分发和安装软件。不幸的是，GUI应用程序通常需要额外的资源来渲染用户界面。为了管理这个挑战，Go应用程序可以将资产捆绑到二进制文件本身中。Fyne工具包更喜欢使用\u0026quot;fyne bundle\u0026quot;，因为它有我们将在下面探索的各种好处。\n捆绑资产的基本方法是执行\u0026quot;fyne bundle\u0026quot;命令。这个工具有各种参数来自定义输出，但在其最基本的形式中，要捆绑的文件将被转换为可以构建到您的应用程序中的Go源代码。\n$ ls image.png\tmain.go $ fyne bundle -o bundled.go image.png $ ls bundled.go\timage.png\tmain.go $ bundled.go的内容将是我们然后可以在代码中访问的资源变量列表。例如，上面的代码将导致包含以下内容的文件：\nvar resourceImagePng = \u0026amp;fyne.StaticResource{ StaticName: \u0026#34;image.png\u0026#34;, StaticContent: []byte{ ... }} 如你所见，默认命名为\u0026quot;resource\u0026lt;Name\u0026gt;.\u0026lt;Ext\u0026gt;\u0026quot;。这个文件中使用的名称和包可以在命令参数中自定义。然后我们可以使用这个名称来，例如，在我们的画布上加载一张图片：\nimg := canvas.NewImageFromResource(resourceImagePng) Fyne资源只是一个带有唯一名称的字节集合，所以这可以是一个字体、一个声音\n","date":"2025-04-26","externalUrl":null,"permalink":"/docs/gofyne/08-extend/03-bundle/","section":"教程","summary":"资源包 # 基于 Go 的应用程序通常构建为单个二进制可执行文件，Fyne应用程序也是如此。单个文件使我们更容易分发和安装软件。不幸的是，GUI应用程序通常需要额外的资源来渲染用户界面。为了管理这个挑战，Go应用程序可以将资产捆绑到二进制文件本身中。Fyne工具包更喜欢使用\"fyne bundle\"，因为它有我们将在下面探索的各种好处。\n","title":"资源包 Bundle","type":"docs"},{"content":"到目前为止，我们已经看到了数据绑定作为一种方式来保持用户界面元素更新。然而，更常见的需求是从UI控件更新值并保持数据在各处都是最新的。值得庆幸的是，Fyne提供的绑定是“双向”的，这意味着值既可以被推入其中也可以被读出。数据的更改将被通知到所有连接的代码，无需任何额外的代码。\n为了看到这个功能的实际操作，我们可以更新最后的测试应用，以显示一个Label和一个Entry，它们绑定到同一个值。通过这样设置，你可以看到通过entry编辑值也会更新label中的文本。这一切都可以在不调用refresh或在代码中引用控件的情况下实现。\n通过将你的应用移动到使用数据绑定，你可以停止保存指向所有控件的指针。相反，通过捕获数据作为一组绑定的值，你的用户界面可以是完全独立的代码。更清晰易读，更易于管理。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/data/binding\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;Two Way\u0026#34;) str := binding.NewString() str.Set(\u0026#34;Hi!\u0026#34;) w.SetContent(container.NewVBox( widget.NewLabelWithData(str), widget.NewEntryWithData(str), )) w.ShowAndRun() } 接下来，我们将看看如何在我们的数据中添加转换。\n","date":"2025-04-08","externalUrl":null,"permalink":"/docs/gofyne/07-binding/02-twoway/","section":"教程","summary":"到目前为止，我们已经看到了数据绑定作为一种方式来保持用户界面元素更新。然而，更常见的需求是从UI控件更新值并保持数据在各处都是最新的。值得庆幸的是，Fyne提供的绑定是“双向”的，这意味着值既可以被推入其中也可以被读出。数据的更改将被通知到所有连接的代码，无需任何额外的代码。\n","title":"双向绑定","type":"docs"},{"content":" 树 Tree # Tree 集合控件类似于 List 控件（工具包的另一个集合控件），具有多级数据结构。像 List 一样，它旨在帮助构建性能良好的接口，以便在展示大量数据时使用。因此，控件不是用所有数据创建的，而是在需要时调用数据源。\nTree 使用回调函数在需要时请求数据。有四个主要的回调，ChildUIDs、IsBranch、CreateNode 和 UpdateNode。ChildUIDs 回调在这里传递每个子节点的唯一 ID 到请求的节点。这将以 TreeNodeID 为 \u0026quot;\u0026quot; 被调用，首先获取所有显示在树根中的 ID 列表。IsBranch 回调应当在节点是分支时返回 true。如果节点 ID 有子节点，通常返回 true - 但你可以有一个空的分支。\n确保树中每个树节点的 id 是唯一的，这一点至关重要。 例如，如果你正在构建一个文件管理器，ID 应该是文件路径而不是其名称。\n其他两个回调与内容模板相关。\nCreateNode 回调返回一个新的模板对象，就像列表一样，尽管有一个额外的 bool 参数，如果节点可以有子元素（是一个分支），则为 true。如前所述，UpdateNode 被调用以将数据应用于单元格模板。你应该根据 TreeNodeID 和 isBranch 参数更新内容。\npackage main import ( \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;树形控件\u0026#34;) tree := widget.NewTree( func(id widget.TreeNodeID) []widget.TreeNodeID { switch id { case \u0026#34;\u0026#34;: return []widget.TreeNodeID{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;} case \u0026#34;a\u0026#34;: return []widget.TreeNodeID{\u0026#34;a1\u0026#34;, \u0026#34;a2\u0026#34;} } return []string{} }, func(id widget.TreeNodeID) bool { return id == \u0026#34;\u0026#34; || id == \u0026#34;a\u0026#34; }, func(branch bool) fyne.CanvasObject { if branch { return widget.NewLabel(\u0026#34;分支模板\u0026#34;) } return widget.NewLabel(\u0026#34;叶子模板\u0026#34;) }, func(id widget.TreeNodeID, branch bool, o fyne.CanvasObject) { text := id if branch { text += \u0026#34; (分支)\u0026#34; } o.(*widget.Label).SetText(text) }) myWindow.SetContent(tree) myWindow.ShowAndRun() } ","date":"2025-03-27","externalUrl":null,"permalink":"/docs/gofyne/06-collection/03-tree/","section":"教程","summary":"树 Tree # Tree 集合控件类似于 List 控件（工具包的另一个集合控件），具有多级数据结构。像 List 一样，它旨在帮助构建性能良好的接口，以便在展示大量数据时使用。因此，控件不是用所有数据创建的，而是在需要时调用数据源。\n","title":"树 Tree","type":"docs"},{"content":"输入控件（Entry widget）用于用户输入简单文本内容。可以通过widget.NewEntry()构造函数简单地创建一个输入控件。创建控件时，保留一个引用，以便以后可以访问其Text字段。还可以使用OnChanged回调函数，每当内容变化时都会收到通知。\n输入控件还可以有验证功能，用于验证输入的文本。这可以通过设置Validator字段为fyne.StringValidator来完成。你还可以设置PlaceHolder文本，并且设置输入控件为MultiLine以接受多行文本。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Entry Widget\u0026#34;) input := widget.NewEntry() input.SetPlaceHolder(\u0026#34;Enter text...\u0026#34;) content := container.NewVBox(input, widget.NewButton(\u0026#34;Save\u0026#34;, func() { log.Println(\u0026#34;Content was:\u0026#34;, input.Text) })) myWindow.SetContent(content) myWindow.ShowAndRun() } 你还可以使用NewPasswordEntry()函数创建一个密码输入控件（内容被隐藏）。\n","date":"2025-03-03","externalUrl":null,"permalink":"/docs/gofyne/05-widget/03-entry/","section":"教程","summary":"输入控件（Entry widget）用于用户输入简单文本内容。可以通过widget.NewEntry()构造函数简单地创建一个输入控件。创建控件时，保留一个引用，以便以后可以访问其Text字段。还可以使用OnChanged回调函数，每当内容变化时都会收到通知。\n","title":"输入框 Entry","type":"docs"},{"content":"与之前的网格布局一样，网格包装布局在网格模式中创建元素的排列。然而，这种网格没有固定数量的列，而是为每个单元格使用固定大小，然后将内容流动到需要显示项目的尽可能多的行中。\n使用 layout.NewGridWrapLayout(size) 创建网格包装布局，其中 size 指定要应用于所有子元素的大小。然后将此布局作为第一个参数传递给 container.New(...)。根据容器的当前大小计算列数和行数。\n最初，网格包装布局将有一个单列，如果您调整它的大小（如代码注释中所示），它将重新排列子元素以填充空间。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/layout\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;网格包装布局\u0026#34;) text1 := canvas.NewText(\u0026#34;1\u0026#34;, color.White) text2 := canvas.NewText(\u0026#34;2\u0026#34;, color.White) text3 := canvas.NewText(\u0026#34;3\u0026#34;, color.White) grid := container.New(layout.NewGridWrapLayout(fyne.NewSize(50, 50)), text1, text2, text3) myWindow.SetContent(grid) // myWindow.Resize(fyne.NewSize(180, 75)) myWindow.ShowAndRun() } ","date":"2025-02-04","externalUrl":null,"permalink":"/docs/gofyne/04-container/03-gridwrap/","section":"教程","summary":"与之前的网格布局一样，网格包装布局在网格模式中创建元素的排列。然而，这种网格没有固定数量的列，而是为每个单元格使用固定大小，然后将内容流动到需要显示项目的尽可能多的行中。\n","title":"网格包装 Grid Wrap","type":"docs"},{"content":"canvas.Line 对象从 Position1（默认是左上角）画到 Position2（默认是右下角）。你可以指定它的颜色，并且可以改变笔触宽度，否则默认为 1。\n线的位置可以使用 Position1 或 Position2 字段，或者使用 Move() 和 Resize() 函数来操作。例如，宽度为 0 的区域会显示为垂直线，而高度为 0 则会是水平线。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;线条\u0026#34;) line := canvas.NewLine(color.White) line.StrokeWidth = 5 w.SetContent(line) w.Resize(fyne.NewSize(100, 100)) w.ShowAndRun() } 线条通常用于自定义布局或手动控制。与文本不同，它们没有自然（最小）大小，但可以在复杂布局中产生出色的效果。\n","date":"2025-01-08","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/03-line/","section":"教程","summary":"canvas.Line 对象从 Position1（默认是左上角）画到 Position2（默认是右下角）。你可以指定它的颜色，并且可以改变笔触宽度，否则默认为 1。\n线的位置可以使用 Position1 或 Position2 字段，或者使用 Move() 和 Resize() 函数来操作。例如，宽度为 0 的区域会显示为垂直线，而高度为 0 则会是水平线。\n","title":"线条 Line","type":"docs"},{"content":"","date":"2024-12-30","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/","section":"教程","summary":"","title":"画图和动画","type":"docs"},{"content":" 手风琴（Accordion） # 手风琴显示一个手风琴项目列表。每个项目由一个按钮表示，当点击时会展示一个详细视图。\n按钮（Button） # 按钮控件有一个文本标签和图标，两者都是可选的。\n卡片（Card） # 卡片控件以头部和副标题将元素分组，所有这些都是可选的。\n复选框（Check） # 复选框控件有一个文本标签和一个选中（或未选中）的图标。\n输入框（Entry） # 输入框控件允许在聚焦时输入简单文本。\n密码输入框控件隐藏文本输入，并添加一个按钮以显示文本。\n文件图标（FileIcon） # 文件图标为各种类型的文件提供有用的标准图标。它显示文件类型的指示图标并显示文件类型的扩展名。\n表单（Form） # 表单控件是一个两列网格，每行有一个标签和一个控件（通常是输入）。如果应该显示任何表单控制按钮，则网格的最后一行将包含它们。\n超链接（Hyperlink） # 超链接控件是一个具有适当填充和布局的文本组件。点击时，URL会在默认网络浏览器中打开。\n图标（Icon） # 图标控件是一个基本的图像组件，加载资源以匹配主题。\n标签（Label） # 标签控件是一个具有适当填充和布局的标签组件。\n进度条（Progress bar） # 进度条控件创建一个表示进度的水平面板。\n无限进度条控件创建一个表示无限等待的水平面板。一个无限进度条会重复从0%循环到100%，直到调用Stop()。\n单选组（RadioGroup） # 单选组控件有一个文本标签列表和每个旁边的单选检查图标。\n选择（Select） # 选择控件有一个选项列表，显示当前选项，并在点击时触发一个事件函数。\n选择输入（SelectEntry） # 选择输入控件在选择控件中添加了一个可编辑组件。用户可以选择一个选项或输入自己的值。\n分隔符（Separator） # 分隔符控件在其他元素之间显示一条分隔线。\n滑块（Slider） # 滑块是一个可以在两个固定值之间滑动的控件。\n文本网格（TextGrid） # 文本网格是一个等宽的字符网格。这是设计用来被文本编辑器、代码预览或终端仿真器使用的。\n工具栏（Toolbar） # 工具栏控件创建一个水平的工具按钮列表。\n集合控件（在widget包中） # 集合控件提供高级缓存功能，以提供大量数据的高性能渲染。这确实导致了更复杂的构造器，但对于它启用的结果来说是一个好平衡。这些控件中的每一个都使用了一系列回调，最小集由它们的构造函数定义，其中包括数据大小，可以重用的模板项目的创建，以及将数据应用到即将添加到显示中的控件的函数。\n列表（List） # 列表提供了许多子项的高性能垂直滚动。\n表格（Table） # 表格提供了许多子项的高性能滚动二维显示。\n树形（Tree） # 树形 控件提供了一个高性能的垂直滚动列表，可以展开以显示子元素。\n容器控件（在container包中） # 容器控件类似于常规容器，但它们提供了一些额外的功能。\n应用标签页 # 应用标签页控件允许从一系列标签项中切换可见内容。每个项都由顶部的一个按钮代表。\n滚动 # 滚动容器定义了一个比内容小的容器。\n分割 # 分割容器定义了一个容器，其大小在两个子项之间分割。\n","date":"2024-12-03","externalUrl":null,"permalink":"/docs/gofyne/02-explore/03-widgets/","section":"教程","summary":"手风琴（Accordion） # 手风琴显示一个手风琴项目列表。每个项目由一个按钮表示，当点击时会展示一个详细视图。\n","title":"内置控件 Widget","type":"docs"},{"content":"如果你想在开始编写自己的应用程序之前看到Fyne工具包的实际效果，你可以查看我们的演示应用程序。\n运行 # 如果愿意，你可以使用以下命令直接运行演示（需要Go 1.16或更高版本）：\ngo run fyne.io/fyne/v2/cmd/fyne_demo@latest 效果图：\n对于早期版本的Go，你需要使用以下命令：\ngo run fyne.io/fyne/v2/cmd/fyne_demo 通过浏览应用的不同标签，你可以看到Fyne工具包的所有功能。\n安装 # 你可以将该应用安装为计算机上的图形应用程序，就像所有其他应用程序一样。我们有一个有用的fyne工具可以为你完成这项工作。首先，你需要安装该工具：\ngo install fyne.io/fyne/v2/cmd/fyne@latest 之后，你可以简单地打包并安装演示应用程序：\nfyne get fyne.io/fyne/v2/cmd/fyne_demo 完成这一步骤后，你可以在应用启动器中找到“Fyne Demo”。\n探索代码 # 如果你对任何功能感兴趣，你应该查看源码仓库或加入社区频道。\n","date":"2024-10-22","externalUrl":null,"permalink":"/docs/gofyne/01-started/03-demo/","section":"教程","summary":"如果你想在开始编写自己的应用程序之前看到Fyne工具包的实际效果，你可以查看我们的演示应用程序。\n运行 # 如果愿意，你可以使用以下命令直接运行演示（需要Go 1.16或更高版本）：\n","title":"运行 Fyne Demo","type":"docs"},{"content":"本文将介绍的 3 个命令，用于提高 Web 开发人员的日常工作效率。\n对 Web 开发而言，除了基本的框架外，日常开发过程中还常用的就是调试工具。本文将要介绍的三个命令分别是 entr、httpie、jq。\nentr，监听文件变化后执行相应命令，实现热重启； httpie，体验更加友好的 HTTP 客户端命令； jq，强大的 JSON 数据解析命令，甚至可简单编程； entr # entr 是一款能轻松实现简单热重启的小工具，具有语言和框架无关性。\n特别说明，这个工具主要是用在开发调试阶段，不支持复杂的热重启能力。\n安装 # brew install entr 简单使用 # 直接通过演示观察它的行为：\n$ ls text.txt | entr echo \u0026#34;file changed\u0026#34; 我们通过 ls text.txt 告诉 entry 监听的文件。当编辑并保存文件后，它会执行后续的命令 echo 打印提示。\n我们只要对它稍做修改，就可以实现在监听到文件变化后，自动执行 停止服务 → 重新编译 → 启动服务 等一系列动作。\n实现热重启 # 首先，开发一个简单 Go server 服务，文件是 main.go，代码如下：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte(\u0026#34;Hello World!\u0026#34;)) }) fmt.Println(\u0026#34;Server is listening on :3000\u0026#34;) http.ListenAndServe(\u0026#34;:3000\u0026#34;, nil) } 为这个服务加上热重启能力：\nfd -e go | entr -r go run *.go 我们通过 fd 遍历所有 go 源码文件，当发现文件更新后会重新执行 go run *.go。-r 实现 reload，它会发送停止信号给常驻服务，让其重新运行。如果希望每次重启后清屏日志，加上 -c 选项。\n支持新建文件 # 但还有个缺点：当创建新文件时，entry 默认无法检测。可以通过 -d 选项解决，它会检测新文件后停止 entr。由于是停止重新执行，要用 while 循环包装：\nwhile true do fd *.go | entr -d -r go run *.go; done 但这样做还有个问题：用 Ctrl+C 强制退出后，while 循环还会重启。需要引入 trap 捕获退出信号：\n#!/bin/bash trap \u0026#34;exit;\u0026#34; SIGINT; while true; do fd -e go | entr -rcd go run *.go; done 把以上内容保存为 run.sh，在源码根目录下运行。现在你的服务就有了实时构建编译和简单的热重启能力。\n限制 # 虽然 entr 能实现简单的热重启机制，但它不具备复杂的状态管理或零停机更新的能力。对于需要更高级热更新的场景，可能需要专门的工具或框架。\nhttpie # HTTPie 是一款注重用户体验的 HTTP 客户端命令，比 curl 更易于使用。\n一个趣事 # 2022 年，HTTPie 的作者误操作将仓库设为私有，导致 54k star 直接清零。两年后重新恢复到 36k star，官方还写了一篇博客反思：How We Lost 54k stars。\n安装 # brew install httpie 安装后有两个命令可用：http 和 https。httpie 命令用于管理自身的扩展和插件。\n基础使用 # # GET 请求 http GET http://httpbin.org/get name==poloxue age==18 # POST 请求 http POST http://httpbin.org/post name=poloxue age=18 易用设计 # httpie 的命令行语法直观易懂。METHOD 在大部分情况下可以省略：\n当有 data 时 → POST，如 http ://httpbin.org/post name=poloxue 当无 data 时 → GET，如 http ://httpbin.org/get 参数设置规则也很清晰：\nheader：使用 key:value 设置 query string：使用 key==value body json data：使用 key=value 即可 # 一次性设置 header、query 和 body http --offline POST http://httpbin.org/post \\ X-API-Token:123456 \\ User-Agent:foo \\ name==poloxue \\ age==18 \\ email=poloxue123@gmail.com --offline 是调试神器，只打印 HTTP 请求文本，不真正发出网络请求。\n输出美化 # HTTPie 默认格式化和高亮响应内容，成功、重定向和错误都会以不同颜色显示。\n# 切换配色风格 http --style autumn GET ://httpbin.org/get 文件下载 # http --download https://github.com/httpie/cli/archive/master.tar.gz http --download -o httpie.tar.gz https://github.com/httpie/cli/archive/master.tar.gz -c 选项启用断点续传。\njq # jq 是一款强大的命令行 JSON 处理器。它让你在终端中对 JSON 数据进行过滤、映射、转换和格式化。在微服务和 API 主导的开发时代，jq 几乎是后端开发的标配工具。\n安装 # # macOS brew install jq # Debian/Ubuntu sudo apt install jq # 或直接下载单二进制文件（无依赖） curl -L https://github.com/jqlang/jq/releases/download/jq-1.7/jq-linux-amd64 -o /usr/local/bin/jq chmod +x /usr/local/bin/jq 格式化 JSON # 这可能是 jq 最常用的场景：\n# 把一行 JSON 格式化为可读格式 echo \u0026#39;{\u0026#34;name\u0026#34;:\u0026#34;poloxue\u0026#34;,\u0026#34;age\u0026#34;:18}\u0026#39; | jq . # 配合 curl 使用 curl -s https://api.github.com/users/poloxue | jq . 选择字段 # # 只提取需要的字段 curl -s https://api.github.com/repos/jqlang/jq \\ | jq \u0026#39;{name: .name, stars: .stargazers_count, license: .license.spdx_id}\u0026#39; 数组处理 # # 列出所有 commits 的作者名 curl -s https://api.github.com/repos/jqlang/jq/commits \\ | jq \u0026#39;.[].commit.author.name\u0026#39; # 筛选条件 curl -s https://api.github.com/repos/jqlang/jq/issues \\ | jq \u0026#39;.[] | select(.state == \u0026#34;open\u0026#34;) | {title, number}\u0026#39; 嵌套结构 # # 展开嵌套字段 echo \u0026#39;{\u0026#34;user\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;alice\u0026#34;,\u0026#34;address\u0026#34;:{\u0026#34;city\u0026#34;:\u0026#34;beijing\u0026#34;,\u0026#34;zip\u0026#34;:\u0026#34;100000\u0026#34;}}}\u0026#39; \\ | jq \u0026#39;.user.address.city\u0026#39; # → \u0026#34;beijing\u0026#34; TSV 输出 # # 以表格形式输出，方便粘贴到 Excel curl -s https://api.github.com/repos/jqlang/jq/issues \\ | jq -r \u0026#39;.[] | [.number, .title, .state, .user.login] | @tsv\u0026#39; 编辑 JSON 文件 # jq 不仅能读，还能写：\n# 添加新字段 cat config.json | jq \u0026#39;. + {\u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;}\u0026#39; \u0026gt; config_new.json # 更新字段 cat config.json | jq \u0026#39;.timeout = 30\u0026#39; \u0026gt; config_new.json # 删除字段 cat config.json | jq \u0026#39;del(.debug)\u0026#39; \u0026gt; config_new.json # 原地编辑（借助 sponge） cat config.json | jq \u0026#39;.timeout = 30\u0026#39; | sponge config.json 实战案例 # 从 CI 日志中提取测试失败的用例：\ncat test-results.json | jq -r \u0026#39; .[] | select(.status == \u0026#34;failed\u0026#34;) | \u0026#34;❌ \\(.name) (\\(.duration)s)\u0026#34; \u0026#39; 配置别名 # # ~/.zshrc alias jqp=\u0026#39;jq -r\u0026#39; # 原始字符串输出 alias jqc=\u0026#39;jq -c\u0026#39; # 紧凑输出 alias json=\u0026#39;jq .\u0026#39; # 快速格式化 JSON ","date":"2024-05-05","externalUrl":null,"permalink":"/docs/mytermenv/commands/dev/","section":"教程","summary":"本文将介绍的 3 个命令，用于提高 Web 开发人员的日常工作效率。\n对 Web 开发而言，除了基本的框架外，日常开发过程中还常用的就是调试工具。本文将要介绍的三个命令分别是 entr、httpie、jq。\n","title":"开发调试","type":"docs"},{"content":"本文是基础篇插件的第二篇，继续介绍 4 个常用的 oh-my-zsh 插件。\n插件 作用 copypath 拷贝当前目录路径到剪贴板 copyfile 拷贝文件内容到剪贴板 dirhistory 用方向键在目录历史中导航 history 更好的历史命令查看方式 在 ~/.zshrc 中加上：\nplugins=(... copypath copyfile dirhistory history) copypath # 有时候需要把当前目录路径粘到某个对话框或文档里。copypath 插件提供了一个 copypath 命令，一键把当前工作目录的绝对路径复制到剪贴板。\ncd ~/Projects/myapp/src copypath # 剪贴板已复制: /Users/poloxue/Projects/myapp/src 搭配 Finder 的\u0026quot;前往文件夹\u0026quot;（Cmd+Shift+G）使用，粘贴路径直达目标目录。\ncopyfile # 当你需要把某个文件的内容复制到剪贴板时，不用 cat file | pbcopy 或鼠标选中，直接用 copyfile：\ncopyfile main.go # main.go 的内容已复制到剪贴板 在粘贴代码到邮件、文档或聊天工具时非常好用。\ndirhistory # 这个插件让你用方向键在目录访问历史中导航：\n快捷键 动作 Alt + ← 回到上一个访问的目录 Alt + → 前进到下一个目录 Alt + ↑ 回到父目录 在几个目录之间来回切换时非常顺手，不用反复 cd .. 再 cd project。\nhistory # oh-my-zsh 默认已经增强了历史命令搜索（输入一部分然后按方向键上），但 history 插件提供了更方便的历史查看：\n# 查看最近的 10 条历史 history 10 # 搜索包含特定关键字的命令 history | grep docker # 查看今天执行的命令 history -t today 小结 # 这 4 个插件专注于剪贴板和导航增强，虽然不是必装，但用上了就能感受到细节上的便利。下一篇开始介绍更强大的社区插件。\n","date":"2024-03-03","externalUrl":null,"permalink":"/docs/mytermenv/ohmyzsh/basicplugins2/","section":"教程","summary":"本文是基础篇插件的第二篇，继续介绍 4 个常用的 oh-my-zsh 插件。\n插件 作用 copypath 拷贝当前目录路径到剪贴板 copyfile 拷贝文件内容到剪贴板 dirhistory 用方向键在目录历史中导航 history 更好的历史命令查看方式 在 ~/.zshrc 中加上：\n","title":"基础插件 2","type":"docs"},{"content":"打开 iTerm2，快速使用体验一下吧。\n分屏功能 # iTerm2 最实用的功能之一就是分屏。不同于在多个窗口间来回切换，分屏让你在一个标签页里同时查看多个终端。\n快捷键 动作 Cmd + D 垂直分屏（左右） Cmd + Shift + D 水平分屏（上下） Cmd + ] / [ 在分屏间切换 分屏的典型场景：\n左边编辑代码，右边运行测试 上方查看日志，下方执行命令 同时监控多个服务器的状态 分屏的大小可以拖动分割线调整，也可以用 Cmd + Ctrl + 方向键 调整当前分屏的大小。\n标签页管理 # iTerm2 的标签页和分屏可以灵活组合：\n快捷键 动作 Cmd + T 新建标签页 Cmd + W 关闭标签页/分屏 Cmd + 数字 切换到第 N 个标签页 Cmd + 方向键 在标签页间切换 一个常用技巧：在标签页里做分屏，不同标签页应对不同任务。比如一个标签页做开发（分屏代码+终端），另一个做运维（分屏多台服务器）。\n搜索与跳转 # Cmd + F 打开搜索，iTerm2 默认支持：\n实时高亮所有匹配项 按 Tab / Shift + Tab 在匹配间跳转 支持正则搜索（搜索框内勾选 Regex） 这个功能在查看长日志时特别有用——搜索错误关键字，然后快速跳转到上下文。\n标记与跳回 # Cmd + Shift + M 可以设置标记（Mark），之后用 Cmd + Shift + J 跳回上一个标记位置。\n想象一下这个场景：你在滚动查看一大段日志输出，中间切出去干了别的事，回来想快速回到刚才看到的位置——一个标记就搞定了。\n选中即复制 # iTerm2 默认开启了\u0026quot;选中即复制\u0026quot;——鼠标选中文本后自动复制到剪贴板，不需要额外的 Cmd + C。这个特性一开始可能不习惯，但用久了会觉得默认 Terminal 的复制方式太繁琐。\n小结 # 以上是 iTerm2 的日常基础操作。掌握分屏、标签页、搜索和标记功能后，你的终端操作效率已经上了一个台阶。下一篇我们来探索 iTerm2 更强大的 Python API。\n","date":"2024-01-21","externalUrl":null,"permalink":"/docs/mytermenv/terminal/usage/","section":"教程","summary":"打开 iTerm2，快速使用体验一下吧。\n分屏功能 # iTerm2 最实用的功能之一就是分屏。不同于在多个窗口间来回切换，分屏让你在一个标签页里同时查看多个终端。\n","title":"开始使用","type":"docs"},{"content":" ohmyzsh # oh-my-zsh 是用于管理 zsh 配置的轻量级框架，具有开箱即用的特点，而且它提供了大量内置插件。让我们用它快速配置 zsh 吧！\noh-my-zsh 这个名字起的很骚气的，大概就是下面这样表情。\n想表达的可能是，当别人看你用 oh-my-zsh 配置的终端，大概率发出 \u0026ldquo;wow! 你的终端太赞了！\u0026rdquo;\n推荐一个网站 # 在开始前，我想先推荐一个 github 仓库，awesome-zsh-plugins，通过浏览器打开 awesome-zsh-plugins，里面提供了相当丰富的 zsh 的框架、教程、插件与主题等等，是 zsh 的资源合集。\n包括如框架、插件和主题等。如 oh-my-zsh，还有其他的一些框架。其中，还有关于 zsh 的教程。除了 oh-my-zsh 内置主题，还有更多主题可选，如将在后面讲介绍的 powerlevel10k 这个 zsh 主题，在这个文档里也能找到。\n","externalUrl":null,"permalink":"/docs/mytermenv/ohmyzsh/","section":"教程","summary":"ohmyzsh # oh-my-zsh 是用于管理 zsh 配置的轻量级框架，具有开箱即用的特点，而且它提供了大量内置插件。让我们用它快速配置 zsh 吧！\n","title":"oh-my-zsh","type":"docs"},{"content":"我的终端环境系列教程重点介绍如何提高我们的日常终端效率。我将会介绍大量工具，从终端、SHELL、命令、会话管理、编辑器等不同展开，以求帮助大家搭建一套高效的终端工作环境。\n","externalUrl":null,"permalink":"/docs/mytermenv/","section":"教程","summary":"我的终端环境系列教程重点介绍如何提高我们的日常终端效率。我将会介绍大量工具，从终端、SHELL、命令、会话管理、编辑器等不同展开，以求帮助大家搭建一套高效的终端工作环境。\n","title":"高效终端","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/","section":"教程","summary":"","title":"快速开始","type":"docs"},{"content":"在 Backtrader 中，许多对象都会生成 Line 对象，每个 Line 代表一个时间序列数据，可以是价格、指标或其他数据。策略逻辑基本都离不开 Line 对象的操作。\nLine 的访问 # 数据源中的 Line # 数据源中包含多个 Line，如 close、open、high、low，通过 self.data.lines 访问它们。\nclass MyStrategy(bt.Strategy): def __init__(self): self.close_line = self.data.lines.close # 访问收盘价线 指标中的 Line # 指标同样会生成 Line，如 SimpleMovingAverage 的 sma，通过 self.movav.lines.sma 访问。\nclass MyStrategy(bt.Strategy): def __init__(self): self.movav = btind.SimpleMovingAverage(self.data, period=20) def next(self): if self.movav.lines.sma[0] \u0026gt; self.data.lines.close[0]: print(\u0026#39;移动平均大于收盘价\u0026#39;) 访问线的快捷方式 # 前面的写法和我们平时使用的不一样，平时都是通过简写访问，如 self.data.close 实际上是 self.data.lines.close 的快捷方式。\nBacktrader 提供了多种简化访问 Line 的方式：\nxxx.lines 可简写 xxx.l； xxx.lines.name 可简写 xxx.lines_name； xxx.lines[0] 可简写为 xxx xxx.lines[X] 可简写为 xxx.lineX； 还有如：\nself.data_name 等同于 self.data.lines.name； 对于有编号的 data，如 self.data1_name，等同于 self.data1.lines.name。 还有 self.dataX_Y 等同于 self.data[X].lines[Y]； 一些常用的简化写法，通过属性访问这些 Line：\nself.data.close 或 self.data_close 直接访问 close 数据行。 self.movav.sma 或 self.movav_sma 直接访问 sma 数据行。 self.movav 也可直接访问 sma 数据行； 这种方式很简洁，但不如原始写法清晰，特别是在区分访问的是否 Line 时。\n线的声明 # 在 Backtrader 中，Line 是指标和策略中非常重要的部分。每个自定义指标都要声明自己的线，以便在策略中使用。\n如何声明线 # 如何在自定义指标中声明线？\n创建自定义指标时，需要通过 lines 属性声明该指标输出的线，通常使用元组声明。\n如在 SimpleMovingAverage 指标中，我们声明了一个名为 sma 的线：\nclass SimpleMovingAverage(bt.Indicator): lines = (\u0026#39;sma\u0026#39;,) # 声明一个名为 sma 的线 def __init__(self): self.lines.sma = self.data.close(-1) # 计算并赋值给 sma 线 如需声明多条线，只需在 lines 中列出多个元素：\nclass MyIndicator(bt.Indicator): lines = (\u0026#39;sma1\u0026#39;, \u0026#39;sma2\u0026#39;) # 声明两个线：sma1 和 sma2 def __init__(self): self.lines.sma1 = btind.SimpleMovingAverage(self.data, period=20) self.lines.sma2 = btind.SimpleMovingAverage(self.data, period=50) 注意：声明 Line 时务必使用元组，即使只有一条线也要加逗号：('sma',)。\n指标的计算结果保存在 Line 中，策略可以直接访问这些 Line 进行交易逻辑判断。\nLine 的长度 # 在 Backtrader 中，每条线都包含一个动态增长的点集合。你可以随时获取线的长度，以了解当前的数据处理情况。\n如何获取线的长度： # 使用 len() 函数：使用标准 Python len() 函数获取 Line 的长度，返回已处理的数据点数。\nclass MyStrategy(bt.Strategy): def next(self): length = len(self.data) print(f\u0026#34;数据源的线长: {length}\u0026#34;) 使用 buflen 属性：buflen 返回数据源加载时的总数据条数。\nclass MyStrategy(bt.Strategy): def next(self): buflen = self.data.buflen print(f\u0026#34;数据源的总长度: {buflen}\u0026#34;) 实盘交易时，buflen 对实时数据源很有用，能显示数据源的实际可用数量。\nlen() 和 buflen 的区别：\nlen() 返回已处理的数据条数，即实际运行到当前时间点的数据长度。 buflen 返回的是数据源加载的总条数，即加载的所有数据的长度。 如果两者的值相同，说明所有数据都已经处理完毕。\nLine 的继承 # Backtrader 支持 Line 的继承，子类可以继承父类的线，也可以在子类中修改。\n示例代码：\nclass BaseIndicator(bt.Indicator): lines = (\u0026#39;sma\u0026#39;,) class MyIndicator(BaseIndicator): lines = (\u0026#39;sma\u0026#39;, \u0026#39;ema\u0026#39;) # 在子类中继承并扩展线 如果多个父类定义了同名线，子类只会继承一个版本，因此要避免线定义冲突。\nLine 耦合 # Backtrader 允许在多个时间框架下使用数据源，并支持将它们的线进行耦合。线耦合是指将不同时间周期的数据结合起来，用于跨时间框架的计算和分析。\n如何使用线耦合： # 可以在策略中同时使用多个数据源，每个数据源可有不同时间周期。例如，一个数据源是日线，另一个是周线：\nclass MyStrategy(bt.Strategy): def __init__(self): self.sma_daily = btind.SimpleMovingAverage(self.data0, period=20) # 日线数据 self.sma_weekly = btind.SimpleMovingAverage(self.data1, period=5) # 周线数据 Backtrader 提供了 () 运算符来将不同时间框架的数据线耦合在一起。\n例如：\nclass MyStrategy(bt.Strategy): def __init__(self): sma0 = btind.SMA(self.data0, period=15) # 15 天的简单移动平均线 sma1 = btind.SMA(self.data1, period=5) # 5 周的简单移动平均线 self.buysig = sma0 \u0026gt; sma1() # 通过运算符将两个时间框架的线耦合 上面的例子中，sma0 和 sma1 分别是基于日线和周线数据计算的简单移动平均线。sma1() 用于将周线数据转换为日线数据长度，从而进行跨时间框架比较。\nLine 耦合让我们能更灵活地在策略中使用多时间框架数据，进行更复杂的分析和决策。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/03-line/","section":"教程","summary":"在 Backtrader 中，许多对象都会生成 Line 对象，每个 Line 代表一个时间序列数据，可以是价格、指标或其他数据。策略逻辑基本都离不开 Line 对象的操作。\nLine 的访问 # 数据源中的 Line # 数据源中包含多个 Line，如 close、open、high、low，通过 self.data.lines 访问它们。\n","title":"Line 数据序列详解","type":"docs"},{"content":"使用模式尽量保持简单。如果策略决定发布订单，可以像这样使用 OCO：\ndef next(self): ... o1 = self.buy(...) ... o2 = self.buy(..., oco=o1) ... o3 = self.buy(..., oco=o1) # 甚至可以是oco=o2，o2已经在o1组中 第一个订单 o1 成为组长。通过 oco 参数，o2 和 o3 成为 OCO 组的一部分。注意，o3 也可以通过指定 oco=o2 加入组（o2 已在组中）。\n组建立后，如果其中任意订单被执行、取消或过期，其他订单将被取消。\n下面的示例展示了OCO概念。一个标准执行并绘图：\n$ ./oco.py --broker cash=50000 --plot 注意\n现金增加到 50000，因为资产价值约 4000，3 个订单各 1 份至少需要 12000（Broker 默认现金为 10000）。\n以下图表基于一个标准的 SMA 交叉策略。\n当快速 SMA 上穿慢速 SMA 时，发布 3 个订单：\norder1：限价订单，limdays 天后到期，限价为收盘价减去一定百分比。 order2：限价订单，期限更长、限价更低。 order3：限价订单，限价更低。 order2 和 order3 不会执行，因为：\norder1 会先执行，触发其他订单取消。 或者 order1 到期，触发其他订单取消。 系统保存 3 个订单的 ref 标识符，只有在 notify_order 中看到三个 ref 全部变为 Completed、Cancelled、Margin 或 Expired 时，才会发布新买单。\n持仓一段时间后简单平仓退出。\n为了跟踪实际执行，生成文本输出。部分内容如下：\n2005-01-28: Oref 1 / Buy at 2941.11055 2005-01-28: Oref 2 / Buy at 2896.7722 2005-01-28: Oref 3 / Buy at 2822.87495 2005-01-31: Order ref: 1 / Type Buy / Status Submitted 2005-01-31: Order ref: 2 / Type Buy / Status Submitted 2005-01-31: Order ref: 3 / Type Buy / Status Submitted 2005-01-31: Order ref: 1 / Type Buy / Status Accepted 2005-01-31: Order ref: 2 / Type Buy / Status Accepted 2005-01-31: Order ref: 3 / Type Buy / Status Accepted 2005-02-01: Order ref: 1 / Type Buy / Status Expired 2005-02-01: Order ref: 3 / Type Buy / Status Canceled 2005-02-01: Order ref: 2 / Type Buy / Status Canceled ... 2006-06-23: Oref 49 / Buy at 3532.39925 2006-06-23: Oref 50 / Buy at 3479.147 2006-06-23: Oref 51 / Buy at 3390.39325 2006-06-26: Order ref: 49 / Type Buy / Status Submitted 2006-06-26: Order ref: 50 / Type Buy / Status Submitted 2006-06-26: Order ref: 51 / Type Buy / Status Submitted 2006-06-26: Order ref: 49 / Type Buy / Status Accepted 2006-06-26: Order ref: 50 / Type Buy / Status Accepted 2006-06-26: Order ref: 51 / Type Buy / Status Accepted 2006-06-26: Order ref: 49 / Type Buy / Status Completed 2006-06-26: Order ref: 51 / Type Buy / Status Canceled 2006-06-26: Order ref: 50 / Type Buy / Status Canceled ... 2006-11-10: Order ref: 61 / Type Buy / Status Canceled 2006-12-11: Oref 63 / Buy at 4032.62555 2006-12-11: Oref 64 / Buy at 3971.8322 2006-12-11: Oref 65 / Buy at 3870.50995 2006-12-12: Order ref: 63 / Type Buy / Status Submitted 2006-12-12: Order ref: 64 / Type Buy / Status Submitted 2006-12-12: Order ref: 65 / Type Buy / Status Submitted 2006-12-12: Order ref: 63 / Type Buy / Status Accepted 2006-12-12: Order ref: 64 / Type Buy / Status Accepted 2006-12-12: Order ref: 65 / Type Buy / Status Accepted 2006-12-15: Order ref: 63 / Type Buy / Status Expired 2006-12-15: Order ref: 65 / Type Buy / Status Canceled 2006-12-15: Order ref: 64 / Type Buy / Status Canceled 结果分析：\n第一批：订单 1 到期，订单 2 和 3 被取消。符合预期。 几个月后第二批：订单 49 完成，订单 50 和 51 立即取消。 最后一批与第一批相同。 现在检查不使用 OCO 的情况：\n$ ./oco.py --strat do_oco=False --broker cash=50000 2005-01-28: Oref 1 / Buy at 2941.11055 2005-01-28: Oref 2 / Buy at 2896.7722 2005-01-28: Oref 3 / Buy at 2822.87495 2005-01-31: Order ref: 1 / Type Buy / Status Submitted 2005-01-31: Order ref: 2 / Type Buy / Status Submitted 2005-01-31: Order ref: 3 / Type Buy / Status Submitted 2005-01-31: Order ref: 1 / Type Buy / Status Accepted 2005-01-31: Order ref: 2 / Type Buy / Status Accepted 2005-01-31: Order ref: 3 / Type Buy / Status Accepted 2005-02-01: Order ref: 1 / Type Buy / Status Expired 输出结果不多（没有订单执行，图表也没太多意义）。\n发布了一批订单。\n订单1过期。但由于 do_oco=False，订单2和3未加入 OCO 组，因此未被取消。默认到期时间 1000 天，在样本数据（2年）内永不过期。\n系统从未发布第二批订单。\n示例使用 # $ ./oco.py --help usage: oco.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] Sample Skeleton optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) 示例代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class St(bt.Strategy): params = dict( ma=bt.ind.SMA, p1=5, p2=15, limit=0.005, limdays=3, limdays2=1000, hold=10, switchp1p2=False, # switch prices of order1 and order2 oco1oco2=False, # False - use order1 as oco for order3, else order2 do_oco=True, # use oco or not ) def notify_order(self, order): print(\u0026#39;{}: Order ref: {} / Type {} / Status {}\u0026#39;.format( self.data.datetime.date(0), order.ref, \u0026#39;Buy\u0026#39; * order.isbuy() or \u0026#39;Sell\u0026#39;, order.getstatusname())) if order.status == order.Completed: self.holdstart = len(self) if not order.alive() and order.ref in self.orefs: self.orefs.remove(order.ref) def __init__(self): ma1, ma2 = self.p.ma(period=self.p.p1), self.p.ma(period=self.p.p2) self.cross = bt.ind.CrossOver(ma1, ma2) self.orefs = list() def next(self): if self.orefs: return # pending orders do nothing if not self.position: if self.cross \u0026gt; 0.0: # crossing up p1 = self.data.close[0] * (1.0 - self.p.limit) p2 = self.data.close[0] * (1.0 - 2 * 2 * self.p.limit) p3 = self.data.close[0] * (1.0 - 3 * 3 * self.p.limit) if self.p.switchp1p2: p1, p2 = p2, p1 o1 = self.buy(exectype=bt.Order.Limit, price=p1, valid=datetime.timedelta(self.p.limdays)) print(\u0026#39;{}: Oref {} / Buy at {}\u0026#39;.format( self.datetime.date(), o1.ref, p1)) oco2 = o1 if self.p.do_oco else None o2 = self.buy(exectype=bt.Order.Limit, price=p2, valid=datetime.timedelta(self.p.limdays2), oco=oco2) print(\u0026#39;{}: Oref {} / Buy at {}\u0026#39;.format( self.datetime.date(), o2.ref, p2)) if self.p.do_oco: oco3 = o1 if not self.p.oco1oco2 else oco2 else: oco3 = None o3 = self.buy(exectype=bt.Order.Limit, price=p3, valid=datetime.timedelta(self.p.limdays2), oco=oco3) print(\u0026#39;{}: Oref {} / Buy at {}\u0026#39;.format( self.datetime.date(), o3.ref, p3)) self.orefs = [o1.ref, o2.ref, o3.ref] else: # in the market if (len(self) - self.holdstart) \u0026gt;= self.p.hold: self.close() def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # Data feed kwargs kwargs = dict() # Parse from/to-date dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # Data feed data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) # Broker cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # Sizer cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # Strategy cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # Execute cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # Plot if requested to cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;Sample Skeleton\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) # Defaults for dates parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/09-orders/04-oco-orders/","section":"教程","summary":"使用模式尽量保持简单。如果策略决定发布订单，可以像这样使用 OCO：\ndef next(self): ... o1 = self.buy(...) ... o2 = self.buy(..., oco=o1) ... o3 = self.buy(..., oco=o1) # 甚至可以是oco=o2，o2已经在o1组中 第一个订单 o1 成为组长。通过 oco 参数，o2 和 o3 成为 OCO 组的一部分。注意，o3 也可以通过指定 oco=o2 加入组（o2 已在组中）。\n","title":"OCO 二选一订单","type":"docs"},{"content":"即使 backtrader 已提供大量内置指标，且开发指标只需定义输入、输出并编写公式，但仍有人希望使用 TA-LIB。原因有二：某些指标在 TA-LIB 中存在但 backtrader 中没有（欢迎提交请求），且 TA-LIB 的行为广为人知，值得信赖。\n为了满足每个人的需求，提供了TA-LIB集成。\n需求 # TA-LIB的Python封装 任何需要的依赖项（例如numpy） 安装详情在GitHub仓库中 使用TA-LIB # 与使用 backtrader 内置指标一样简单。以下是简单移动平均线的示例，先看 backtrader 版本：\nimport backtrader as bt class MyStrategy(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 20),) def __init__(self): self.sma = bt.indicators.SMA(self.data, period=self.p.period) ... 接下来是TA-LIB的示例：\nimport backtrader as bt class MyStrategy(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 20),) def __init__(self): self.sma = bt.talib.SMA(self.data, timeperiod=self.p.period) ... TA-LIB 指标的参数由库本身定义，而非 backtrader。例如，TA-LIB 的 SMA 使用名为 timeperiod 的参数来定义窗口大小。\n对于需要多个输入的指标，例如随机指标：\nimport backtrader as bt class MyStrategy(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 20),) def __init__(self): self.stoc = bt.talib.STOCH(self.data.high, self.data.low, self.data.close, fastk_period=14, slowk_period=3, slowd_period=3) ... 注意，high、low和close分别传递。可以尝试传递open而不是low（或其他任何数据系列）进行实验。\nTA-LIB指标文档会自动解析并添加到backtrader文档中。你也可以查看TA-LIB的源代码/文档，或者执行以下操作：\nprint(bt.talib.SMA.__doc__) 输出如下：\nSMA([input_arrays], [timeperiod=30]) Simple Moving Average (Overlap Studies) Inputs: price: (any ndarray) Parameters: timeperiod: 30 Outputs: real 这提供了一些信息：\n期望的输入（忽略ndarray的注释，因为backtrader在后台进行转换） 哪些参数及其默认值 指标实际提供的输出线 移动平均线和MA_Type # 要选择特定的移动平均线，如bt.talib.STOCH，标准的TA-LIB MA_Type可以通过backtrader.talib.MA_Type访问。例如：\nimport backtrader as bt print(\u0026#39;SMA:\u0026#39;, bt.talib.MA_Type.SMA) print(\u0026#39;T3:\u0026#39;, bt.talib.MA_Type.T3) 绘制TA-LIB指标 # 与常规用法一样，绘制TA-LIB指标无需特殊操作。\n输出蜡烛图的指标（所有识别蜡烛图形态的指标）会生成二进制输出：0 或 100。为了避免增加子图，自动绘图转换会在识别到模式时将其绘制在数据上。\n示例和比较 # 以下是一些比较TA-LIB指标输出与backtrader内置指标的示例。注意：\nTA-LIB指标在图上有一个TA_前缀。这是样例特意这样做以帮助用户区分。 如果两者结果相同，移动平均线会叠加在现有移动平均线上，无法单独查看，这样的测试是通过的。 所有样例包括CDLDOJI指标作为参考。\nKAMA（考夫曼移动平均线） # 这是唯一直接比较中存在差异的指标：\n样本的初始值不同 在某个时间点，值会趋同，两个KAMA实现具有相同的行为。 分析 TA-LIB 源码发现：\nTA-LIB的实现为KAMA的初始值做了一个非行业标准的选择。 源代码引用：这里使用昨天的价格作为前一个KAMA。 backtrader采用了通常的选择，例如Stockcharts：\nStockCharts上的KAMA 由于需要一个初始值来开始计算，第一个KAMA只是一个简单移动平均线。因此存在差异。此外：\nTA-LIB 的 KAMA 实现不允许指定调整 Kaufman 定义中可缩放常数的快慢周期。 样本执行：\n$ ./talibtest.py --plot --ind kama 输出 图像\nSMA # $ ./talibtest.py --plot --ind sma 输出 图像\nEMA # $ ./talibtest.py --plot --ind ema 输出 图像\n随机指标 # $ ./talibtest.py --plot --ind stoc 输出 图像\nRSI # $ ./talibtest.py --plot --ind rsi 输出 图像\nMACD # $ ./talibtest.py --plot --ind macd 输出 图像\n布林带 # $ ./talibtest.py --plot --ind bollinger 输出 图像\nAROON # 注意，TA-LIB选择先绘制下降线，并且颜色与backtrader内置指标相反。\n$ ./talibtest.py --plot --ind aroon 输出 图像\nUltimate Oscillator # $ ./talibtest.py --plot --ind ultimate 输出 图像\nTrix # $ ./talibtest.py --plot --ind trix 输出 图像\nADXR # backtrader同时提供ADX和ADXR线。\n$ ./talibtest.py --plot --ind adxr 输出 图像\nDEMA # $ ./talibtest.py --plot --ind dema 输出 图像\nTEMA # $ ./talibtest.py --plot --ind tema 输出 图像\nPPO # backtrader不仅提供PPO线，还提供更传统的MACD方法。\n$ ./talibtest.py --plot --ind ppo 输出 图像\nWilliamsR # $ ./talibtest.py --plot --ind williamsr 输出 图像\nROC # 所有指标应具有相同的形状，但跟踪动量或变化率有多种定义。\n$ ./talibtest.py --plot --ind roc 输出 图像\n样本用法 # $ ./talibtest.py --help usage: talibtest.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--ind {sma,ema,stoc,rsi,macd,bollinger,aroon,ultimate,trix,kama,adxr,dema,tema,ppo,williamsr,roc}] [--no-doji] [--use-next] [--plot [kwargs]] Sample for ta-lib optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to be read in (default: ../../datas/yhoo-1996-2015.txt) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: 2005-01-01) --todate TODATE Ending date in YYYY-MM-DD format (default: 2006-12-31) --ind {sma,ema,stoc,rsi,macd,bollinger,aroon,ultimate,trix,kama,adxr,dema,tema,ppo,williamsr,roc} Which indicator pair to show together (default: sma) --no-doji Remove Doji CandleStick pattern checker (default: False) --use-next Use next (step by step) instead of once (batch) (default: False) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example (escape the quotes if needed): --plot style=\u0026#34;candle\u0026#34; (to plot candles) (default: None) 样本代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class TALibStrategy(bt.Strategy): params = ((\u0026#39;ind\u0026#39;, \u0026#39;sma\u0026#39;), (\u0026#39;doji\u0026#39;, True),) INDS = [\u0026#39;sma\u0026#39;, \u0026#39;ema\u0026#39;, \u0026#39;stoc\u0026#39;, \u0026#39;rsi\u0026#39;, \u0026#39;mac d\u0026#39;, \u0026#39;bollinger\u0026#39;, \u0026#39;aroon\u0026#39;, \u0026#39;ultimate\u0026#39;, \u0026#39;trix\u0026#39;, \u0026#39;kama\u0026#39;, \u0026#39;adxr\u0026#39;, \u0026#39;dema\u0026#39;, \u0026#39;ppo\u0026#39;, \u0026#39;tema\u0026#39;, \u0026#39;roc\u0026#39;, \u0026#39;williamsr\u0026#39;] def __init__(self): if self.p.doji: bt.talib.CDLDOJI(self.data.open, self.data.high, self.data.low, self.data.close) if self.p.ind == \u0026#39;sma\u0026#39;: bt.talib.SMA(self.data.close, timeperiod=25, plotname=\u0026#39;TA_SMA\u0026#39;) bt.indicators.SMA(self.data, period=25) elif self.p.ind == \u0026#39;ema\u0026#39;: bt.talib.EMA(timeperiod=25, plotname=\u0026#39;TA_SMA\u0026#39;) bt.indicators.EMA(period=25) elif self.p.ind == \u0026#39;stoc\u0026#39;: bt.talib.STOCH(self.data.high, self.data.low, self.data.close, fastk_period=14, slowk_period=3, slowd_period=3, plotname=\u0026#39;TA_STOCH\u0026#39;) bt.indicators.Stochastic(self.data) elif self.p.ind == \u0026#39;macd\u0026#39;: bt.talib.MACD(self.data, plotname=\u0026#39;TA_MACD\u0026#39;) bt.indicators.MACD(self.data) bt.indicators.MACDHisto(self.data) elif self.p.ind == \u0026#39;bollinger\u0026#39;: bt.talib.BBANDS(self.data, timeperiod=25, plotname=\u0026#39;TA_BBANDS\u0026#39;) bt.indicators.BollingerBands(self.data, period=25) elif self.p.ind == \u0026#39;rsi\u0026#39;: bt.talib.RSI(self.data, plotname=\u0026#39;TA_RSI\u0026#39;) bt.indicators.RSI(self.data) elif self.p.ind == \u0026#39;aroon\u0026#39;: bt.talib.AROON(self.data.high, self.data.low, plotname=\u0026#39;TA_AROON\u0026#39;) bt.indicators.AroonIndicator(self.data) elif self.p.ind == \u0026#39;ultimate\u0026#39;: bt.talib.ULTOSC(self.data.high, self.data.low, self.data.close, plotname=\u0026#39;TA_ULTOSC\u0026#39;) bt.indicators.UltimateOscillator(self.data) elif self.p.ind == \u0026#39;trix\u0026#39;: bt.talib.TRIX(self.data, timeperiod=25, plotname=\u0026#39;TA_TRIX\u0026#39;) bt.indicators.Trix(self.data, period=25) elif self.p.ind == \u0026#39;adxr\u0026#39;: bt.talib.ADXR(self.data.high, self.data.low, self.data.close, plotname=\u0026#39;TA_ADXR\u0026#39;) bt.indicators.ADXR(self.data) elif self.p.ind == \u0026#39;kama\u0026#39;: bt.talib.KAMA(self.data, timeperiod=25, plotname=\u0026#39;TA_KAMA\u0026#39;) bt.indicators.KAMA(self.data, period=25) elif self.p.ind == \u0026#39;dema\u0026#39;: bt.talib.DEMA(self.data, timeperiod=25, plotname=\u0026#39;TA_DEMA\u0026#39;) bt.indicators.DEMA(self.data, period=25) elif self.p.ind == \u0026#39;ppo\u0026#39;: bt.talib.PPO(self.data, plotname=\u0026#39;TA_PPO\u0026#39;) bt.indicators.PPO(self.data, _movav=bt.indicators.SMA) elif self.p.ind == \u0026#39;tema\u0026#39;: bt.talib.TEMA(self.data, timeperiod=25, plotname=\u0026#39;TA_TEMA\u0026#39;) bt.indicators.TEMA(self.data, period=25) elif self.p.ind == \u0026#39;roc\u0026#39;: bt.talib.ROC(self.data, timeperiod=12, plotname=\u0026#39;TA_ROC\u0026#39;) bt.talib.ROCP(self.data, timeperiod=12, plotname=\u0026#39;TA_ROCP\u0026#39;) bt.talib.ROCR(self.data, timeperiod=12, plotname=\u0026#39;TA_ROCR\u0026#39;) bt.talib.ROCR100(self.data, timeperiod=12, plotname=\u0026#39;TA_ROCR100\u0026#39;) bt.indicators.ROC(self.data, period=12) bt.indicators.Momentum(self.data, period=12) bt.indicators.MomentumOscillator(self.data, period=12) elif self.p.ind == \u0026#39;williamsr\u0026#39;: bt.talib.WILLR(self.data.high, self.data.low, self.data.close, plotname=\u0026#39;TA_WILLR\u0026#39;) bt.indicators.WilliamsR(self.data) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() dkwargs = dict() if args.fromdate: fromdate = datetime.datetime.strptime(args.fromdate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;fromdate\u0026#39;] = fromdate if args.todate: todate = datetime.datetime.strptime(args.todate, \u0026#39;%Y-%m-%d\u0026#39;) dkwargs[\u0026#39;todate\u0026#39;] = todate data0 = bt.feeds.YahooFinanceCSVData(dataname=args.data0, **dkwargs) cerebro.adddata(data0) cerebro.addstrategy(TALibStrategy, ind=args.ind, doji=not args.no_doji) cerebro.run(runcone=not args.use_next, stdstats=False) if args.plot: pkwargs = dict(style=\u0026#39;candle\u0026#39;) if args.plot is not True: # evals to True but is not True npkwargs = eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;) # args were passed pkwargs.update(npkwargs) cerebro.plot(**pkwargs) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Sample for sizer\u0026#39;) parser.add_argument(\u0026#39;--data0\u0026#39;, required=False, default=\u0026#39;../../datas/yhoo-1996-2015.txt\u0026#39;, help=\u0026#39;Data to be read in\u0026#39;) parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;2005-01-01\u0026#39;, help=\u0026#39;Starting date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;2006-12-31\u0026#39;, help=\u0026#39;Ending date in YYYY-MM-DD format\u0026#39;) parser.add_argument(\u0026#39;--ind\u0026#39;, required=False, action=\u0026#39;store\u0026#39;, default=TALibStrategy.INDS[0], choices=TALibStrategy.INDS, help=(\u0026#39;Which indicator pair to show together\u0026#39;)) parser.add_argument(\u0026#39;--no-doji\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Remove Doji CandleStick pattern checker\u0026#39;)) parser.add_argument(\u0026#39;--use-next\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Use next (step by step) \u0026#39; \u0026#39;instead of once (batch)\u0026#39;)) # Plot options parser.add_argument(\u0026#39;--plot\u0026#39;, \u0026#39;-p\u0026#39;, nargs=\u0026#39;?\u0026#39;, required=False, metavar=\u0026#39;kwargs\u0026#39;, const=True, help=(\u0026#39;Plot the read data applying any kwargs passed\\n\u0026#39; \u0026#39;\\n\u0026#39; \u0026#39;For example (escape the quotes if needed):\\n\u0026#39; \u0026#39;\\n\u0026#39; \u0026#39; --plot style=\u0026#34;candle\u0026#34; (to plot candles)\\n\u0026#39;)) if pargs is not None: return parser.parse_args(pargs) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() 这样，你就可以在backtrader中使用TA-LIB的指标，并根据需要进行绘图和比较。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/08-indicators/04-talib/","section":"教程","summary":"即使 backtrader 已提供大量内置指标，且开发指标只需定义输入、输出并编写公式，但仍有人希望使用 TA-LIB。原因有二：某些指标在 TA-LIB 中存在但 backtrader 中没有（欢迎提交请求），且 TA-LIB 的行为广为人知，值得信赖。\n","title":"TA-Lib 技术指标集成","type":"docs"},{"content":"Visual Chart 的集成支持以下功能：\n实时数据馈送 实时交易 Visual Chart 是一个集图表、数据馈送和经纪功能于一体的完整交易平台。\n更多信息请访问：www.visualchart.com\n要求 # VisualChart 6（运行在 Windows 上） comtypes fork： https://github.com/mementum/comtypes 可以通过以下命令安装：\npip install https://github.com/mementum/comtypes/archive/master.zip Visual Chart 的 API 基于 COM。目前 comtypes 主分支不支持解包 VT_ARRAYS of VT_RECORD（Visual Chart 正使用此特性）。Pull Request #104 已提交但尚未合并。合并后即可使用主分支。\npytz（可选但强烈推荐）：确保数据以市场时间返回。这对大多数市场成立，但有些市场是例外（如全球指数）。 示例代码 # 源代码中包含完整示例：samples/vctest/vctest.py。\nVCStore - 存储 # 存储是实时数据馈送/交易支持的核心，提供了 COM API 和数据馈送及经纪代理之间的适配层。\n可以通过以下方法获取经纪商实例：\nVCStore.getbroker(*args, **kwargs) 可以通过以下方法获取数据馈送实例：\nVCStore.getdata(*args, **kwargs) 在这种情况下，许多 **kwargs 是数据馈送的常见参数，如 dataname、fromdate、todate、sessionstart、sessionend、timeframe、compression。\nVCStore 会尝试：\n通过 Windows 注册表自动定位系统中 VisualChart 的安装位置。 如果找到，扫描安装目录中的 COM DLL，创建 COM typelib 并实例化相关对象。 如果未找到，使用已知的硬编码 CLSID 进行相同操作。 注意：即使找到 DLL，Visual Chart 本身也必须正在运行。backtrader 不会自动启动 Visual Chart。\nVCData 数据馈送 # 一般说明 # Visual Chart 的数据馈送有以下特性：\n重采样由平台完成 但不适用于所有情况：秒级别不支持，仍需 backtrader 处理 例如：\nvcstore = bt.stores.VCStore() vcstore.getdata(dataname=\u0026#39;015ES\u0026#39;, timeframe=bt.TimeFrame.Ticks) cerebro.resampledata(data, timeframe=bt.TimeFrame.Seconds, compression=5) 大多数情况下，只需以下操作：\nvcstore = bt.stores.VCStore() data = vcstore.getdata(dataname=\u0026#39;015ES\u0026#39;, timeframe=bt.TimeFrame.Minutes, compression=2) cerebro.adddata(data) 数据通过比较内部设备时钟与平台提供的 tick 来计算时间偏移，以便在没有新 tick 时尽早传递自动重采样的 K 线。\n实例化数据 # 按 VisualChart 左上角显示的符号传递（无空格）。例如：\nES-Mini 显示为 001 ES，实例化为：\ndata = vcstore.getdata(dataname=\u0026#39;001ES\u0026#39;, ...) EuroStoxx 50 显示为 015 ES，实例化为：\ndata = vcstore.getdata(dataname=\u0026#39;015ES\u0026#39;, ...) 注意：backtrader 会自动清理第 4 位的空格（如果名称直接复制自 Visual Chart）。\n时间管理 # 时间管理遵循 backtrader 的一般规则。为了确保代码不依赖于 DST 转换，请使用市场时间。\n数据通知 # 数据馈送通过以下方式报告当前状态（详见 Cerebro 和策略参考）：\nCerebro.notify_data（如果覆盖） Cerebro.adddatacb 添加的回调 Strategy.notify_data（如果覆盖） 策略中的示例：\nclass VCStrategy(bt.Strategy): def notify_data(self, data, status, *args, **kwargs): if status == data.LIVE: # 数据已切换到实时数据 # 做某些事 pass 系统状态变化时发送以下通知：\nCONNECTED：成功初始连接 DISCONNECTED：无法检索数据 CONNBROKEN：连接丢失 NOTSUBSCRIBED：无权限检索数据 DELAYED：历史/回填操作进行中 LIVE：数据已切换为实时数据 VCBroker - 实时交易 # 使用经纪商 # 要使用 VCBroker，需替换 cerebro 创建的默认模拟经纪商。\n使用存储模型（推荐）：\nimport backtrader as bt cerebro = bt.Cerebro() vcstore = bt.stores.VCStore() cerebro.broker = vcstore.getbroker() # 或 cerebro.setbroker(...) 经纪商参数 # VCBroker 不支持任何参数，因为经纪商只是实际经纪商的代理，不应屏蔽真实功能。\n限制 # 头寸 # Visual Chart 报告未平仓头寸，但缺少头寸关闭的最终事件。因此，backtrader 需要自行记录头寸，并与账户中已有的头寸分开管理。\n佣金 # COM 交易接口不报告佣金。除非在实例化经纪商时提供了 Commission 实例，否则 backtrader 无法准确估算佣金。\n交易操作 # 使用方法与回测一致。使用策略中的标准方法：\nbuy sell close cancel 订单执行类型 # Visual Chart 支持 backtrader 所需的最小订单执行类型，确保回测内容可上线。支持的类型：\nOrder.Market Order.Close Order.Limit Order.Stop（触发后跟随市价单） Order.StopLimit（触发后跟随限价单） 订单有效期 # 回测中的有效期概念（valid 参数）在此同样适用。\n通知 # 标准订单状态通过策略的 notify_order 方法通知（如果覆盖）：\nSubmitted - 订单已发送 Accepted - 订单已放置 Rejected - 订单放置失败或生命周期内被取消 Partial - 已部分执行 Completed - 已完全执行 Canceled（或 Cancelled） 参考 # VCStore # class backtrader.stores.VCStore() 封装 ibpy ibConnection 实例的单例类。\nVCBroker # class backtrader.brokers.VCBroker(**kwargs) VisualChart 的经纪商实现。\nVCData # class backtrader.feeds.VCData(**kwargs) VisualChart 数据馈送。\n参数：\nqcheck（默认：0.5）：默认唤醒超时以让重采样/重放器检查当前 bar 是否可以交付。 historical（默认：False）：如果未提供 todate 参数（在基类中定义），则如果设置为 True，将强制仅进行历史下载。 milliseconds（默认：True）：Visual Chart 构建的 bar 具有以下格式：HH:MM:59.999000。如果该参数为 True，将添加一毫秒，使其显示为：HH:MM:59.999000。 tradename（默认：无）：无法交易连续期货，但它们非常适合数据跟踪。如果提供该参数，它将是当前期货的名称，该期货将是交易资产。 usetimezones（默认：True）：对于大多数市场，Visual Chart 提供的时间偏移信息允许将日期时间转换为市场时间。 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/15-livetrading/03-visual-chart/","section":"教程","summary":"Visual Chart 的集成支持以下功能：\n实时数据馈送 实时交易 Visual Chart 是一个集图表、数据馈送和经纪功能于一体的完整交易平台。\n更多信息请访问：www.visualchart.com\n要求 # VisualChart 6（运行在 Windows 上） comtypes fork： https://github.com/mementum/comtypes 可以通过以下命令安装：\n","title":"Visual Chart 实盘接入","type":"docs"},{"content":"Backtrader 的 Broker 模拟在订单执行时有一个默认策略：忽略成交量。这是基于两个假设：\n交易在流动性足够高的市场中进行，可一次性完全吸收买卖订单。 实际的成交量匹配需要真实市场环境。 一个简单的例子是”立即成交或取消”（Fill or Kill）订单。即使逐笔细化且有足够的成交量，Backtrader 的 Broker 模拟也无法知道市场中有多少其他参与者，无法判断这样的订单是否能立即成交或应取消。\n但 Broker 模拟可以接受成交量填充器（Volume Fillers），由它们决定在给定时间点使用多少成交量来匹配订单。\n填充器签名 # 在 Backtrader 生态系统中，填充器可以是任何符合以下签名的可调用对象：\ncallable(order, price, ago) 其中：\norder：即将执行的订单，提供对目标数据对象的访问，包括创建的大小/价格、执行的价格/大小/剩余大小等。 price：订单执行的价格。 ago：数据在订单中的索引，用于查找成交量和价格数据。 在大多数情况下，ago 为 0（当前时间点），但在某些特殊情况下（如 Close 订单），可能为 -1。\n例如，访问 bar 的成交量：\nbarvolume = order.data.volume[ago] 可调用对象可以是一个函数，或例如支持__call__方法的类的实例，例如：\nclass MyFiller(object): def __call__(self, order, price, ago): pass 将填充器添加到 Broker 模拟\n最直接的方式是使用 set_filler：\nimport backtrader as bt cerebro = Cerebro() cerebro.broker.set_filler(bt.broker.fillers.FixedSize()) 第二种方式是完全替换 Broker 模拟，这通常用于重写了部分功能的 BrokerBack 子类：\nimport backtrader as bt cerebro = Cerebro() filler = bt.broker.fillers.FixedSize() newbroker = bt.broker.BrokerBack(filler=filler) cerebro.broker = newbroker 示例 # Backtrader的源代码中包含一个名为volumefilling的示例，它允许测试一些集成的填充器（最初是全部）。\n参考 # class backtrader.fillers.FixedSize() 使用 bar 中一定百分比的成交量返回订单的执行大小，百分比由参数 perc 设定。\n参数：\nsize（默认：None）：最大执行大小。如果执行时 bar 的实际成交量小于该值，则以实际成交量为限。\n如果该参数的值为 False，则使用 bar 的全部成交量来匹配订单。\nclass backtrader.fillers.FixedBarPerc() 使用 bar 中一定百分比的成交量返回订单的执行大小，百分比通过 perc 参数设置。\n参数：\nperc（默认：100.0）（有效值：0.0 - 100.0）\n用于执行订单的 bar 成交量百分比\nclass backtrader.fillers.BarPointPerc() 返回给定订单的执行大小。成交量将在高-低范围内均匀分布，使用 minmov 进行分区。\n对于给定价格分配的成交量，使用 perc 百分比。\n参数：\nminmov（默认：0.01）：最小价格变动。用于分区高-低范围，按比例分配各价位之间的成交量。\nperc（默认：100.0）（有效值：0.0 - 100.0）：用于订单执行价格匹配的分配成交量百分比。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/10-broker/04-volume-filling-filler/","section":"教程","summary":"Backtrader 的 Broker 模拟在订单执行时有一个默认策略：忽略成交量。这是基于两个假设：\n交易在流动性足够高的市场中进行，可一次性完全吸收买卖订单。 实际的成交量匹配需要真实市场环境。 一个简单的例子是”立即成交或取消”（Fill or Kill）订单。即使逐笔细化且有足够的成交量，Backtrader 的 Broker 模拟也无法知道市场中有多少其他参与者，无法判断这样的订单是否能立即成交或应取消。\n","title":"成交量填充器 Filler","type":"docs"},{"content":"最近 reddit/r/algotrading 上有讨论，关于能否成功复现已发布的算法交易策略。首先，我复现了 130 多篇关于”预测股市”的研究论文，从头编写代码并记录了结果。以下是一些收获：\n之前的帖子已被删除，以下是快速总结：\n策略无效\n如果作者声称策略因阿尔法衰减失效，那么这些测试都在过去的数据上运行过，但它们仍然无效。 结论：这些策略要么是过拟合，要么是 p-hacking，或者只有微小的阿尔法，而这些阿尔法已被交易佣金吞噬。 Artem Kaznatcheev 在《算法交易中的复现危机寓言》中描述了复现问题，接着写了关于过拟合的文章。\n前两篇文章主要是理论性的（即使第一篇提到实现了 130 个策略），而《过拟合》提供了实际代码。\n与其讨论论文，不如尝试像《过拟合》那样，实际复现一些知名书籍中发布的策略。\n目标：”打败随机入场”。这是该书第三部分第八章的一节：\n《Amazon - 交易你的财务自由之路》\nVan Tharp Institute - 《交易你的财务自由之路》\n该书提供了结构化的算法交易方法，特别强调仓位大小和仓位管理（即何时退出交易）。这些远比入场设置更为关键。\n在第八章中，Van K. Tharp 与 Tom Basso 交谈时说：”从你说的来看，听起来只要你有好的退出方式和聪明的仓位管理，似乎可以通过随机入场持续赚钱。” Tom Basso 回应说他确实可以做到。\n规则：\n基于硬币投掷的入场 始终在市场中——多头或空头 一旦退出信号给出，立即重新进入 市场波动性由10天的平均真实波幅指数（EMA ATR）决定 跟踪止损，距离收盘价3倍波动性 止损只能朝着交易方向移动 固定仓位（1个合约）或1%风险模型（见书第12章） 结果：\n测试10个市场 固定仓位：80%的时间获利 1%风险模型：100%的时间获利 可靠性：38%（获胜交易的百分比） 缺失部分：\n测试的市场 测试的时间段 “始终在市场中”通常最容易克服——无论是指”今天”关闭并在”明天”重新进入，还是同时发出关闭/开仓订单。\n关于后两项，书中提到对话发生在1991年，使用期货。为公平起见，使用 1991 年前的期货数据，并假设使用 1 天价格 K 线（考虑到提到的 10 天 EMA）。\n最明显的问题是如何正确实现算法。不过，书籍很好地描述了简单算法及其结果。接下来总结第 12 章的”百分比风险模型”（书中称为”模型 3”）。\n最大亏损：限制为账户总值的x%（即：百分比风险）\n每合约风险：根据给定的算法，风险为初始止损距离（波动性的3倍）乘以期货的乘数\n合约数量：最大亏损 / 每合约风险\n复现细节 # 数据\n使用 1985 至 1990 年的 CL（原油）期货数据（6 年完整数据）。合约规格：\n每点最小变动：0.01（即每点 100 个 tick） 每个 tick 费用：10 美元 乘数：1000 美元/点（100 ticks/点 x 10 美元/tick = 1000 美元） 佣金\n每个合约每次交易 2.00 货币单位（类似 IB 的收费方式）\n实现细节\n硬币投掷建模为指示器，以便可视化投掷发生的位置（如果多个入场方向相同，符合随机性预期）。\n为可视化止损及其移动方式，止损价格计算和逻辑也嵌入到指示器中。止损逻辑有两个阶段：\n交易开始时，止损价格与收盘价保持给定距离，与之前的止损价格无关。 交易进行中，止损价格在可能时调整以跟随趋势。 图表显示\n代码会生成两种类型的图表：\n单次测试运行图表（\u0026ndash;plot选项）。通常在运行单次迭代（\u0026ndash;iterations 1）时使用它最为合适。 散点图，显示运行的盈亏情况。 样本#1\n测试运行图表：\n**** Iteration: 1 -- PNL: 10482.00 -- Trades 49 - Won 22 - %_Won: 0.45 **** Summary of Runs -- Total : 1 -- Won : 1 -- % Won : 1.00 **** Summary of Trades -- Total : 49 -- Total Won : 22 -- % Total Won : 0.45 样本#2： 100次使用1%风险模型的测试运行，10次迭代和散点图\n**** Iteration: 1 -- PNL: -18218.00 -- Trades 60 - Won 24 - %_Won: 0.40 **** Iteration: 2 ... **** Iteration: 100 -- PNL: 111366.00 -- Trades 50 - Won 26 - %_Won: 0.52 **** Summary of Runs -- Total : 100 -- Won : 50 -- % Won : 0.50 **** Summary of Trades -- Total : 5504 -- Total Won : 2284 -- % Total Won : 0.41 测试运行混合\n对10次测试运行进行了100次迭代，混合了以下变量：\n固定仓位为1，或使用1%的百分比风险模型 在同一根K线内执行入场/退出，或在连续K线内执行 结果总结\n平均而言，49%的测试运行是盈利的。固定仓位的盈利率接近50%，而百分比风险模型的盈利率波动较大，某些测试运行的盈利率最低为39%，最高为65%。 平均来说，39%的交易是盈利的（波动较小）。 回顾书中的结果：\n固定仓位模型：80% 的盈利运行 1% 风险模型：100% 的盈利运行 38% 的盈利交易 因此，似乎只有最后一项得到了复现。\n结论\n正如 Artem Kaznatcheev 所指出的，复现危机可能源于：\n使用了错误的数据集 未正确实现算法 或者原始实现可能未完全遵循其规则，或并非所有细节都已公开。 注意\n无论如何，个人仍然推荐阅读这本书。未能复现特定案例并不意味着这本书不好，书中展示了算法交易的实际方法。\n完整脚本\n享受吧！\n代码也可以在以下链接找到：GitHub Gist\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/19-articles/04-beating-the-random-entry/","section":"教程","summary":"最近 reddit/r/algotrading 上有讨论，关于能否成功复现已发布的算法交易策略。首先，我复现了 130 多篇关于”预测股市”的研究论文，从头编写代码并记录了结果。以下是一些收获：\n","title":"打败随机入场策略","type":"docs"},{"content":" AnnualReturn # class backtrader.analyzers.AnnualReturn() 该分析器通过查看每年的起始和结束值来计算年度回报率。\n参数：\n无 成员属性：\nrets：计算出的年度回报率列表 ret：年度回报率字典（键：年份） get_analysis： 返回包含年度回报率的字典（键：年份）\nCalmar # class backtrader.analyzers.Calmar() 该分析器计算 Calmar 比率，时间框架可以与数据使用的不一致。\n参数：\ntimeframe（默认：无）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：无）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个包含时间段键和对应滚动 Calmar 比率的 OrderedDict。\nDrawDown # class backtrader.analyzers.DrawDown() 该分析器计算交易系统的回撤统计数据，包括百分比和货币单位的回撤值、最大回撤值、回撤长度和最大回撤长度。\n参数：\nfund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个包含回撤统计数据的字典（支持 . 符号表示法和子字典），可用的键/属性包括：\ndrawdown：回撤值（百分比） moneydown：回撤值（货币单位） len：回撤长度 max.drawdown：最大回撤值（百分比） max.moneydown：最大回撤值（货币单位） max.len：最大回撤长度 TimeDrawDown # class backtrader.analyzers.TimeDrawDown() 该分析器在选定时间框架上计算交易系统的回撤，时间框架可以与数据使用的不一致。\n参数：\ntimeframe（默认：无）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：无）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个包含回撤统计数据的字典（支持 . 符号表示法和子字典），可用的键/属性包括：\ndrawdown：回撤值（百分比） maxdrawdown：最大回撤值（货币单位） maxdrawdownperiod：回撤长度 GrossLeverage # class backtrader.analyzers.GrossLeverage() 该分析器计算当前策略的总杠杆。\n参数：\nfund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期时间点为键。\nPositionsValue # class backtrader.analyzers.PositionsValue() 该分析器报告当前数据集的仓位价值。\n参数：\ntimeframe（默认：无）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：无）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 headers（默认：False）：在保存结果的字典中添加一个初始键，名称为数据的名称（\u0026lsquo;Datetime\u0026rsquo; 为键）。 cash（默认：False）：包括实际现金作为额外仓位（对于标头，将使用名称 \u0026lsquo;cash\u0026rsquo;）。 get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期时间点为键。\nPyFolio # class backtrader.analyzers.PyFolio() 该分析器使用 4 个子分析器收集数据，并将其转换为 pyfolio 兼容的数据集。\n子分析器：\nTimeReturn：用于计算全球投资组合价值的回报。 PositionsValue：用于计算每个数据的仓位价值。设置 headers 和 cash 参数为 True。 Transactions：用于记录每个数据上的每笔交易（数量、价格、价值）。设置 headers 参数为 True。 GrossLeverage：跟踪总杠杆（策略投资的程度）。 参数： 这些参数透明地传递给子分析器。\ntimeframe（默认：bt.TimeFrame.Days）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：1）：如果为 None，将使用系统中第一个数据的压缩。 get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期时间点为键。\nget_pf_items： 返回一个包含 4 个元素的元组，可用于进一步处理 pyfolio。\nreturns，positions，transactions，gross_leverage 由于这些对象旨在作为 pyfolio 的直接输入，此方法会本地导入 pandas，将 backtrader 内部结果转换为 pandas DataFrames，这是 pyfolio.create_full_tear_sheet 等函数预期的输入。如果未安装 pandas，该方法将失败。\nLogReturnsRolling # class backtrader.analyzers.LogReturnsRolling() 该分析器计算给定时间框架和压缩的滚动回报。\n参数：\ntimeframe（默认：无）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：无）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 data（默认：无）：跟踪的参考资产，而不是投资组合价值。 get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期时间点为键。\nPeriodStats # class backtrader.analyzers.PeriodStats() 计算给定时间框架的基本统计数据。\n参数：\ntimeframe（默认：Years）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：1）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个包含以下键的字典：\naverage stddev positive negative nochange best worst 如果参数 zeroispos 设置为 True，则没有变化的周期将计为正数。\nReturns # class backtrader.analyzers.Returns() 使用对数方法计算总回报、平均回报、复合回报和年化回报。\n参数：\ntimeframe（默认：无）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：无）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 tann（默认：无）：用于年化（归一化）回报的周期数： 天：252 周：52 月：12 年：1 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期\n时间点为键。返回的字典包含以下键：\nrtot：总复合回报 ravg：整个期间的平均回报（特定时间框架） rnorm：年化/归一化回报 rnorm100：以 100% 表示的年化/归一化回报 SharpeRatio # class backtrader.analyzers.SharpeRatio() 该分析器使用无风险资产（即利率）计算策略的夏普比率。\n参数：\ntimeframe（默认：TimeFrame.Years） compression（默认：1）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 riskfreerate（默认：0.01 -\u0026gt; 1%）：以年利率表示（见下文 convertrate）。 convertrate（默认：True）：将年利率转换为月、周或日利率。不支持子日转换。 factor（默认：无）：如果为 None，将从预定义表中选择年到所选时间框架的转换因子。天：252，周：52，月：12，年：1。否则将使用指定的值。 annualize（默认：False）：如果 convertrate 为 True，夏普比率将在所选时间框架内提供。在大多数情况下，夏普比率以年化形式提供。 stddev_sample（默认：False）：如果设置为 True，将在均值中减少分母 1 来计算标准差。这在计算标准差时使用，如果认为并非所有样本都用于计算。这被称为贝塞尔修正。 daysfactor（默认：无）：旧命名为因子。如果设置为除 None 之外的任何值，并且时间框架为 TimeFrame.Days，将假设这是旧代码并使用该值。 legacyannual（默认：False）：使用 AnnualReturn 分析器，顾名思义仅适用于年份。 fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个包含键 “sharperatio” 的字典，其中包含比率。\nSharpeRatio_A # class backtrader.analyzers.SharpeRatio_A() 夏普比率的扩展，直接以年化形式返回夏普比率。\n更改的参数：\nannualize（默认：True） SQN # class backtrader.analyzers.SQN() SQN（System Quality Number，系统质量数）。由 Van K. Tharp 定义，用于分类交易系统。\n1.6 - 1.9 低于平均水平 2.0 - 2.4 平均水平 2.5 - 2.9 良好 3.0 - 5.0 优秀 5.1 - 6.9 杰出 7.0 - 圣杯？\n公式：\nSquareRoot(NumberTrades) * Average(TradesProfit) / StdDev(TradesProfit) 当交易数量 \u0026gt;= 30 时，sqn 值应被认为是可靠的。\nget_analysis： 返回一个包含键 “sqn” 和 “trades” 的字典（已考虑的交易数量）。\nTimeReturn # class backtrader.analyzers.TimeReturn() 该分析器通过查看时间框架的起点和终点来计算回报。\n参数：\ntimeframe（默认：无）：如果为 None，将使用系统中第一个数据的时间框架。 compression（默认：无）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 data（默认：无）：跟踪的参考资产，而不是投资组合价值。 get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期时间点为键。\nTradeAnalyzer # class backtrader.analyzers.TradeAnalyzer() 提供已平仓交易的统计数据（同时也保持未平仓交易的计数）。\n总开仓/平仓交易 连胜/连败 当前/最长 总损益/平均损益 胜/负 计数/总损益/平均损益/最大损益 多/空 计数/总损益/平均损益/最大损益 胜/负 计数/总损益/平均损益/最大损益 注意： 分析器使用“自动”字典字段，这意味着如果没有执行交易，则不会生成统计数据。在这种情况下，get_analysis 返回的字典中将有一个单独的字段/子字段：\ndictname[‘total’][‘total’] 将具有值 0（该字段也可以使用点符号 dictname.total.total 访问）。 Transactions # class backtrader.analyzers.Transactions() 该分析器报告系统中每个数据的交易情况。\n参数：\nheaders（默认：True）：在保存结果的字典中添加一个初始键，名称为数据的名称。 该分析器旨在便于与 pyfolio 集成，标题名称取自用于它的样本：\n\u0026lsquo;date\u0026rsquo;, \u0026lsquo;amount\u0026rsquo;, \u0026lsquo;price\u0026rsquo;, \u0026lsquo;sid\u0026rsquo;, \u0026lsquo;symbol\u0026rsquo;, \u0026lsquo;value\u0026rsquo; get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期时间点为键。\nVWR # class backtrader.analyzers.VWR() 可变性加权回报（Variability Weighted Return）：使用对数回报改进的夏普比率。\n别名：\nVariabilityWeightedReturn 参数：\ntimeframe（默认：无）：如果为 None，则整个回测期间的回报将被报告。 compression（默认：无）：仅用于子日时间框架，例如通过指定 \u0026ldquo;TimeFrame.Minutes\u0026rdquo; 和 60 作为压缩在每小时时间框架上工作。 tann（默认：无）：用于年化（归一化）平均回报的周期数。如果为 None，则使用标准 t 值，即： 天：252 周：52 月：12 年：1 tau（默认：2.0）：计算因子（见文献） sdev_max（默认：0.20）：最大标准差（见文献） fund（默认：无）：如果为 None，将自动检测经纪人的实际模式（fundmode - True/False）来决定回报率是基于总净资产价值还是基金价值。见经纪人文档中的 set_fundmode。 get_analysis： 返回一个字典，其中返回值为值，每个返回值的日期时间点为键。返回的字典包含以下键：\nvwr：可变性加权回报 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/12-analyzers/04-reference/","section":"教程","summary":"AnnualReturn # class backtrader.analyzers.AnnualReturn() 该分析器通过查看每年的起始和结束值来计算年度回报率。\n参数：\n无 成员属性：\nrets：计算出的年度回报率列表 ret：年度回报率字典（键：年份） get_analysis： 返回包含年度回报率的字典（键：年份）\n","title":"分析器 API 参考文档","type":"docs"},{"content":"注意：示例中使用的 Binary 文件 goog.fd 属于 VisualChart，不能与 backtrader 一起分发。\n对于那些有兴趣直接使用 Binary 文件的人，可以免费下载 VisualChart。\nCSV 数据源开发展示了如何添加基于 CSV 的数据源。基类 CSVDataBase 提供了框架，子类只需执行：\ndef _loadline(self, linetokens): # 在这里解析 linetokens 并将它们放入 self.lines.close, self.lines.high 等中 return True # 如果数据已解析，否则返回 False 基类负责参数、初始化、打开文件、读取行、标记化等事项，还会跳过不在日期范围（fromdate, todate）内的行。\n开发非 CSV 数据源遵循相同的模式，而无需深入到已拆分的行标记。\n需要做的事情： # 从 backtrader.feed.DataBase 派生 添加任何需要的参数 如果需要初始化，重写 __init__(self) 和/或 start(self) 如果需要清理代码，重写 stop(self) 工作发生在必须始终重写的方法 _load(self) 内 让我们看看 backtrader.feed.DataBase 已经提供的参数：\nfrom backtrader.utils.py3 import with_metaclass ... ... class DataBase(with_metaclass(MetaDataBase, dataseries.OHLCDateTime)): params = ((\u0026#39;dataname\u0026#39;, None), (\u0026#39;fromdate\u0026#39;, datetime.datetime.min), (\u0026#39;todate\u0026#39;, datetime.datetime.max), (\u0026#39;name\u0026#39;, \u0026#39;\u0026#39;), (\u0026#39;compression\u0026#39;, 1), (\u0026#39;timeframe\u0026#39;, TimeFrame.Days), (\u0026#39;sessionend\u0026#39;, None)) 这些参数具有以下含义：\ndataname：数据源识别如何获取数据的依据。在 CSVDataBase 中表示文件路径或类似文件的对象。 fromdate 和 todate：定义传递给策略的日期范围，超出此范围的数据将被忽略。 name：绘图时使用的名称。 timeframe：时间框架参考。可选值：Ticks, Seconds, Minutes, Days, Weeks, Months, Years。 compression（默认值：1）：每个实际条的压缩条数。仅在数据重采样/重放中有效。 sessionend：如果传入 datetime.time 对象，将添加到数据源的日期时间行，用于标识会话结束。 示例二进制数据源 # backtrader 已经定义了 VChartCSVData 用于 VisualChart 的导出数据，但也可以直接读取二进制数据文件。\n下面来实现（完整代码见文末）。\n初始化 # 二进制 VisualChart 数据文件包含每日数据（.fd 扩展名）或日内数据（.min 扩展名），通过参数 timeframe 区分文件类型。\n在 __init__ 中为每种类型设置不同的常量。\ndef __init__(self): super(VChartData, self).__init__() # 使用 informative \u0026#34;timeframe\u0026#34; 参数来理解传递的 \u0026#34;dataname\u0026#34; # 是指日内还是每日数据源 if self.p.timeframe \u0026gt;= TimeFrame.Days: self.barsize = 28 self.dtsize = 1 self.barfmt = \u0026#39;IffffII\u0026#39; else: self.dtsize = 2 self.barsize = 32 self.barfmt = \u0026#39;IIffffII\u0026#39; 开始 # 数据源在回测开始时启动（优化期间可能启动多次）。\n在 start 方法中，除非传递了类似文件的对象，否则二进制文件会被打开。\ndef start(self): # 数据源必须启动...打开文件（或查看是否已打开） self.f = None if hasattr(self.p.dataname, \u0026#39;read\u0026#39;): # 传入了文件（例如：来自 GUI） self.f = self.p.dataname else: # 让异常传播 self.f = open(self.p.dataname, \u0026#39;rb\u0026#39;) 停止 # 回测结束时调用。\n如果文件已打开，则将其关闭。\ndef stop(self): # 如果有文件，关闭它 if self.f is not None: self.f.close() self.f = None 实际加载 # 实际工作在 _load 中完成，用于加载下一组数据：datetime、open、high、low、close、volume、openinterest。在 backtrader 中，\u0026ldquo;当前\u0026quot;时刻对应于索引 0。\n从文件中读取指定数量的字节（由 __init__ 中的常量确定），使用 struct 模块解析，进行必要的处理（如日期时间的 divmod 操作），并存入数据源的行中。\n如果无法读取数据，视为文件末尾（EOF），返回 False。\n如果数据加载并解析成功，返回 True。\ndef _load(self): if self.f is None: # 如果没有文件...无法解析 return False # 读取所需数量的二进制数据 bardata = self.f.read(self.barsize) if not bardata: # 如果没有读取数据...游戏结束返回 \u0026#34;False\u0026#34; return False # 使用 struct 解析数据 bdata = struct.unpack(self.barfmt, bardata) # 年份存储为每年 500 天 y, md = divmod(bdata[0], 500) # 月份存储为每月 32 天 m, d = divmod(md, 32) # 将 y, m, d 放入 datetime dt = datetime.datetime(y, m, d) if self.dtsize \u0026gt; 1: # 分钟条 # 每日时间以秒为单位存储 hhmm, ss = divmod(bdata[1], 60) hh, mm = divmod(hhmm, 60) # 将时间添加到现有的 datetime dt = dt.replace(hour=hh, minute=mm, second=ss) self.lines.datetime[0] = date2num(dt) # 获取解析的数据的其余部分 o, h, l, c, v, oi = bdata[self.dtsize:] self.lines.open[0] = o self.lines.high[0] = h self.lines.low[0] = l self.lines.close[0] = c self.lines.volume[0] = v self.lines.openinterest[0] = oi # 返回成功 return True 其他二进制格式 # 同样的模式适用于其他二进制源：\n数据库 分层数据存储 在线来源 步骤总结：\n__init__ -\u0026gt; 实例的初始化代码，只执行一次 start -\u0026gt; 回测开始时执行（优化时将多次运行） stop -\u0026gt; 清理工作，如关闭数据库连接或套接字 _load -\u0026gt; 从数据库或在线源获取下一组数据，加载到行对象中。标准字段：datetime、open、high、low、close、volume、openinterest VChartData 测试 # VChartData 从本地 .fd 文件加载 2006 年 Google 的数据。\n仅涉及加载数据，因此不需要策略的子类。\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import datetime import backtrader as bt from vchart import VChartData if __name__ == \u0026#39;__main__\u0026#39;: # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) # 添加策略 cerebro.addstrategy(bt.Strategy) ########################################################################### # 注意： # goog.fd 文件属于 VisualChart，不能与 backtrader 一起分发 # # VisualChart 可从 www.visualchart.com 下载 ########################################################################### # 创建数据源 datapath = \u0026#39;../../datas/goog.fd\u0026#39; data = VChartData( dataname=datapath, fromdate=datetime.datetime(2006, 1, 1), todate=datetime.datetime(2006, 12, 31), timeframe=bt.TimeFrame.Days ) # 将数据源添加到 Cerebro cerebro.adddata(data) # 运行所有内容 cerebro.run() # 绘制结果 cerebro.plot(style=\u0026#39;bar\u0026#39;) VChartData 完整代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime import struct from backtrader.feed import DataBase from backtrader import date2num from backtrader import TimeFrame class VChartData(DataBase): def __init__(self): super(VChartData, self).__init__() # 使用 informative \u0026#34;timeframe\u0026#34; 参数来理解传递的 \u0026#34;dataname\u0026#34; # 是指日内还是每日数据源 if self.p.timeframe \u0026gt;= TimeFrame.Days: self.barsize = 28 self.dtsize = 1 self.barfmt = \u0026#39;IffffII\u0026#39; else: self.dtsize = 2 self.barsize = 32 self.barfmt = \u0026#39;IIffffII\u0026#39; def start(self): # 数据源必须启动...打开文件（或查看是否已打开） self.f = None if hasattr(self.p.dataname, \u0026#39;read\u0026#39;): # 传入了文件（例如：来自 GUI） self.f = self.p.dataname else: # 让异常传播 self.f = open(self.p.dataname, \u0026#39;rb\u0026#39;) def stop(self): # 如果有文件，关闭它 if self.f is not None: self.f.close() self.f = None def _load(self): if self.f is None: # 如果没有文件...无法解析 return False # 读取所需数量的二进制数据 bardata = self.f.read(self.barsize) if not bardata: # 如果没有读取数据...游戏结束返回 \u0026#34;False\u0026#34; return False # 使用 struct 解析数据 bdata = struct.unpack(self.barfmt, bardata) # 年份存储为每年 500 天 y, md = divmod(bdata[0], 500) # 月份存储为每月 32 天 m, d = divmod(md, 32) # 将 y, m, d 放入 datetime dt = datetime.datetime(y, m, d) if self.dtsize \u0026gt; 1: # 分钟条 # 每日时间以秒为单位存储 hhmm, ss = divmod(bdata[1], 60) hh, mm = divmod(hhmm, 60) # 将时间添加到现有的 datetime dt = dt.replace(hour=hh, minute=mm, second=ss) self.lines.datetime[0] = date2num(dt) # 获取解析的数据的其余部分 o, h, l, c, v, oi = bdata[self.dtsize:] self.lines.open[0] = o self.lines.high[0] = h self.lines.low[0] = l self.lines.close[0] = c self.lines.volume[0] = v self.lines.openinterest[0] = oi # 返回成功 return True ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/04-datafeed-development-general/","section":"教程","summary":"注意：示例中使用的 Binary 文件 goog.fd 属于 VisualChart，不能与 backtrader 一起分发。\n对于那些有兴趣直接使用 Binary 文件的人，可以免费下载 VisualChart。\nCSV 数据源开发展示了如何添加基于 CSV 的数据源。基类 CSVDataBase 提供了框架，子类只需执行：\n","title":"开发 Binary 二进制数据源","type":"docs"},{"content":"Backtrader 的开发是在大内存机器上进行的，加上绘图可视化反馈非常有用（几乎是必需品），这使得设计决策变得简单：将所有数据保存在内存中。但这一决定有一些缺点：\n使用 array.array 存储数据时，超出某些边界后需要重新分配和移动数据。 内存较少的机器可能会受到影响。 连接到一个可能运行数周/月的实时数据源，提供大量秒/分钟级别的 tick 数据。 后者比前者更重要，因为backtrader做出了另一个设计决策：\n保持纯Python以便在需要时能够在嵌入式系统中运行。 未来可能出现这样的场景：backtrader 连接到第二台机器获取实时数据，而自身运行在 Raspberry Pi 甚至资源更受限的设备上，如 ADSL 路由器（带有 Freetz 映像的 AVM Frit!Box 7490）。\n因此，backtrader 需要支持动态内存管理。现在可以在实例化或运行 Cerebro 时使用以下设置：\nexactbars 默认值为 False，每条线中存储的值都会保存在内存中。可能的值：\nTrue或1：将所有 Line 对象的内存使用减少到自动计算的最小周期。\n如果 SMA 的周期为30，则底层将始终有一个30条的运行缓冲区，以允许计算 SMA。 此设置将停用预加载和runonce。 使用此设置还将停用绘图。 -1：在策略级别的数据源和指标/操作将保留所有数据在内存中。\n例如 RSI 内部使用指标 UpDay 计算，但 UpDay 不保留所有数据在内存中。 这允许保持绘图和预加载活动。 runonce将被停用。 -2：作为策略属性的数据源和指标将保留所有点在内存中。\n例如 RSI 内部使用指标 UpDay 计算，但 UpDay 不保留所有数据在内存中。 如果在__init__中定义了a = self.data.close - self.data.high，那么a将不保留所有数据在内存中。 这允许保持绘图和预加载活动。 runonce将被停用。 俗话说，百闻不如一见。下面通过示例脚本来展示差异。该脚本针对 1996 年至 2015 年的雅虎每日数据运行，共计 4965 天。\n注意：这是一个小样本。交易 14 小时的 EuroStoxx50 期货在一个月内将生成约 18000 个 1 分钟 K 线。\n首先执行脚本，查看在不请求内存节省时使用了多少内存位置：\n$ ./memory-savings.py --save 0 Total memory cells used: 506430 对于级别1（完全节省）：\n$ ./memory-savings.py --save 1 Total memory cells used: 2041 从 50 万降到 2041，节省效果显著。\n系统中的每个 Line 对象使用 collections.deque 作为缓冲区（而非 array.array），长度限制为操作所需的最小值。\n例如，使用周期为 30 的简单移动平均线的策略，会进行以下调整：\n数据源将有一个 30 位的缓冲区，这是 SMA 生成下一个值所需的大小。 SMA 将只有一个位置的缓冲区，因为除非有其他依赖于它的指标需要更多数据，否则无需保留更大的缓冲区。 注意：此模式最吸引人且可能最重要的特点是，整个脚本生命周期内使用的内存量保持不变。\n无论数据源的大小如何，如果长时间连接实时数据源，这将非常有用。\n但请注意：\n绘图不可用。 还有其他内存消耗源，如策略生成的订单，随着时间的推移会积累。 此模式只能在 Cerebro 中与 runonce=False 配合使用。这对实时数据源也是强制性的，但在简单回测时比 runonce=True 慢。从某个临界点开始，内存管理的开销可能超过逐步执行回测的成本，但具体情况需要由用户自行判断。 接下来是负级别，旨在保持绘图可用的同时节省大量内存。首先是级别 -1：\n$ ./memory-savings.py --save -1 Total memory cells used: 184623 在这种情况下，一级指标（策略中直接声明的指标）保留完整长度的缓冲区。但如果这些指标依赖于其他子指标（实际情况正是如此），则子指标的长度将受限。结果从：\n506430 内存位置降至 184623 节省超过 50%。\n注意：array.array 对象已被 collections.deque 替换，后者虽然内存开销更大，但操作速度更快。不过 collections.deque 对象本身很小，节省的内存位置大致接近估算值。\n接下来是级别 -2，旨在节省策略级别声明且标记为不绘图的指标的内存：\n$ ./memory-savings.py --save -2 Total memory cells used: 174695 现在并没有节省多少。这是因为只有一个指标被标记为不绘图：TestInd().plotinfo.plot = False。\n让我们看看最后一个示例的绘图：\n$ ./memory-savings.py --save -2 --plot Total memory cells used: 174695 感兴趣的读者可运行示例脚本，详细分析每个 Line 对象，遍历整个指标层次结构。启用绘图运行（级别 -1）：\n$ ./memory-savings.py --save -1 --lendetails -- Evaluating Datas ---- Data 0 Total Cells 34755 - Cells per Line 4965 -- Evaluating Indicators ---- Indicator 1.0 Average Total Cells 30 - Cells per line 30 ---- SubIndicators Total Cells 1 ---- Indicator 1.1 _LineDelay Total Cells 1 - Cells per line 1 ---- SubIndicators Total Cells 1 ... ---- Indicator 0.5 TestInd Total Cells 9930 - Cells per line 4965 ---- SubIndicators Total Cells 0 -- Evaluating Observers ---- Observer 0 Total Cells 9930 - Cells per Line 4965 ---- Observer 1 Total Cells 9930 - Cells per Line 4965 ---- Observer 2 Total Cells 9930 - Cells per Line 4965 Total memory cells used: 184623 相同的，但启用最大节省（1）：\n$ ./memory-savings.py --save 1 --lendetails -- Evaluating Datas ---- Data 0 Total Cells 266 - Cells per Line 38 -- Evaluating Indicators ---- Indicator 1.0 Average Total Cells 30 - Cells per line 30 ---- SubIndicators Total Cells 1 ... ---- Indicator 0.5 TestInd Total Cells 2 - Cells per line 1 ---- SubIndicators Total Cells 0 -- Evaluating Observers ---- Observer 0 Total Cells 2 - Cells per Line 1 ---- Observer 1 Total Cells 2 - Cells per Line 1 ---- Observer 2 Total Cells 2 - Cells per Line 1 第二个输出显示，数据源中的线已被限制为 38 个内存位置，而非 4965 个（完整数据源长度）。\n并且在可能的情况下，指标和观察者已被限制为 1 个内存位置，如输出最后几行所示。\n脚本代码和使用 # 在backtrader的源代码中提供了示例。使用方法：\n$ ./memory-savings.py --help usage: memory-savings.py [-h] [--data DATA] [--save SAVE] [--datalines] [--lendetails] [--plot] Check Memory Savings optional arguments: -h, --help show this help message and exit --data DATA Data to be read in (default: ../../datas/yhoo-1996-2015.txt) --save SAVE Memory saving level [1, 0, -1, -2] (default: 0) --datalines Print data lines (default: False) --lendetails Print individual items memory usage (default: False) --plot Plot the result (default: False) 代码：\nfrom __future__ import (absolute_import , division, print_function, unicode_literals) import argparse import sys import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind import backtrader.utils.flushfile class TestInd(bt.Indicator): lines = (\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;) def __init__(self): self.lines.a = b = self.data.close - self.data.high self.lines.b = btind.SMA(b, period=20) class St(bt.Strategy): params = ( (\u0026#39;datalines\u0026#39;, False), (\u0026#39;lendetails\u0026#39;, False), ) def __init__(self): btind.SMA() btind.Stochastic() btind.RSI() btind.MACD() btind.CCI() TestInd().plotinfo.plot = False def next(self): if self.p.datalines: txt = \u0026#39;,\u0026#39;.join( [\u0026#39;%04d\u0026#39; % len(self), \u0026#39;%04d\u0026#39; % len(self.data0), self.data.datetime.date(0).isoformat()] ) print(txt) def loglendetails(self, msg): if self.p.lendetails: print(msg) def stop(self): super(St, self).stop() tlen = 0 self.loglendetails(\u0026#39;-- Evaluating Datas\u0026#39;) for i, data in enumerate(self.datas): tdata = 0 for line in data.lines: tdata += len(line.array) tline = len(line.array) tlen += tdata logtxt = \u0026#39;---- Data {} Total Cells {} - Cells per Line {}\u0026#39; self.loglendetails(logtxt.format(i, tdata, tline)) self.loglendetails(\u0026#39;-- Evaluating Indicators\u0026#39;) for i, ind in enumerate(self.getindicators()): tlen += self.rindicator(ind, i, 0) self.loglendetails(\u0026#39;-- Evaluating Observers\u0026#39;) for i, obs in enumerate(self.getobservers()): tobs = 0 for line in obs.lines: tobs += len(line.array) tline = len(line.array) tlen += tdata logtxt = \u0026#39;---- Observer {} Total Cells {} - Cells per Line {}\u0026#39; self.loglendetails(logtxt.format(i, tobs, tline)) print(\u0026#39;Total memory cells used: {}\u0026#39;.format(tlen)) def rindicator(self, ind, i, deep): tind = 0 for line in ind.lines: tind += len(line.array) tline = len(line.array) thisind = tind tsub = 0 for j, sind in enumerate(ind.getindicators()): tsub += self.rindicator(sind, j, deep + 1) iname = ind.__class__.__name__.split(\u0026#39;.\u0026#39;)[-1] logtxt = \u0026#39;---- Indicator {}.{} {} Total Cells {} - Cells per line {}\u0026#39; self.loglendetails(logtxt.format(deep, i, iname, tind, tline)) logtxt = \u0026#39;---- SubIndicators Total Cells {}\u0026#39; self.loglendetails(logtxt.format(deep, i, iname, tsub)) return tind + tsub def runstrat(): args = parse_args() cerebro = bt.Cerebro() data = btfeeds.YahooFinanceCSVData(dataname=args.data) cerebro.adddata(data) cerebro.addstrategy( St, datalines=args.datalines, lendetails=args.lendetails) cerebro.run(runonce=False, exactbars=args.save) if args.plot: cerebro.plot(style=\u0026#39;bar\u0026#39;) def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Check Memory Savings\u0026#39;) parser.add_argument(\u0026#39;--data\u0026#39;, required=False, default=\u0026#39;../../datas/yhoo-1996-2015.txt\u0026#39;, help=\u0026#39;Data to be read in\u0026#39;) parser.add_argument(\u0026#39;--save\u0026#39;, required=False, type=int, default=0, help=(\u0026#39;Memory saving level [1, 0, -1, -2]\u0026#39;)) parser.add_argument(\u0026#39;--datalines\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Print data lines\u0026#39;)) parser.add_argument(\u0026#39;--lendetails\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Print individual items memory usage\u0026#39;)) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=(\u0026#39;Plot the result\u0026#39;)) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/05-cerebro/03-saving-memory/","section":"教程","summary":"Backtrader 的开发是在大内存机器上进行的，加上绘图可视化反馈非常有用（几乎是必需品），这使得设计决策变得简单：将所有数据保存在内存中。但这一决定有一些缺点：\n","title":"内存优化与节省技巧","type":"docs"},{"content":" 本文具体讲讲如何在 Backtrader 中配置数据源，也就是 DataFeed。\n我会先演示如何给 Backtrader 添加 CSV 数据源。然后，再介绍 Backtrader 中最常用的两种 DataFeed 的使用。\n添加数据 # 给 Backtrader 加载数据源分为两步：创建 DataFeed，然后通过 cerebro.adddata 加载到系统。\ndata = bt.feeds.{DataFeedClass} cerebro.adddata(data) Backtrader 提供了种类繁多的 DataFeedClass，可用于对接处理不同的数据源，如 CSV 文件、Pandas DataFrame 或者其他自定义的数据类。\n下面用 CSV 数据文件作为演示。先从 Backtrader 仓库下载 Oracle 1995 年至 2014 年的行情数据文件 orcl-1995-2014.csv。\n开始编写代码吧。\n使用 bt.feeds.GenericCSVData 读取 CSV 文件：\ndata = bt.feeds.GenericCSVData( dataname=\u0026#34;./orcl-1995-2014.txt\u0026#34;, dtformat=\u0026#34;%Y-%m-%d\u0026#34;, ) 指定数据文件的路径和设置日期格式。\nGenericCSVData 能适应各种格式的 CSV 文件，是一个通用的读取 CSV 行情数据的 DataFeed 类。\n创建 DataFeed 后，通过 cerebro.adddata(data) 将其加载到回测引擎中。\ncerebro.adddata(data) 完整脚本：\nimport backtrader as bt def main(): cerebro = bt.Cerebro() data = bt.feeds.GenericCSVData( dataname=\u0026#34;./orcl-1995-2014.txt\u0026#34;, dtformat=\u0026#34;%Y-%m-%d\u0026#34;, ) cerebro.adddata(data) cerebro.run() cerebro.plot() if __name__ == \u0026#34;__main__\u0026#34;: main() 运行代码后，图表中就能看到价格行情了。\nPandas 数据源 # 前面通过 GenericCSVData 加载远程 CSV 文件。Backtrader 还支持其他数据获取方式，最常用的是基于 Pandas DataFrame 创建 DataFeed。\n依赖 pandas 的灵活性，这种方式能极大扩展数据渠道，无论是本地文件、关系型数据库还是远程 API（如 yfinance、tushare、akshare）都能搞定。\n好，我们直接来看代码。\n现在用 yfinance 下载 BTC-USD 从 2024-01-01 到 2025-11-10 的行情数据：\ncerebro = bt.Cerebro() df = yf.download(\u0026#34;BTC-USD\u0026#34;, start=\u0026#34;2024-01-01\u0026#34;, end=\u0026#39;2025-11-10\u0026#39;, multi_level_index=False) data = bt.feeds.PandasData(dataname=df) cerebro.adddata(data) cerebro.run() cerebro.plot(style=\u0026#39;bar\u0026#39;) 注：使用新版 yfinance 时需设置 multi_level_index=False，否则多级索引可能导致加载失败。\n输出绘图如下所示：\n如上所示，先用 yfinance 获取指定品种和时间段的数据得到 DataFrame，然后传递给 PandasData 即可创建 Backtrader 可用的数据源。\nPandasData 对传入的数据有一定要求：索引为时间，数据列需包含 open、high、low、close、volume 等。列名大写（如 Open、High、Low、Close）也可，PandasData 内部会自动转为小写。\n准备工作做完后，就可以用 bt.feeds.PandasData 包装 DataFrame，然后通过 cerebro.adddata() 添加到回测系统中。\n小结 # Backtrader 的数据能力非常强大，本文只是简单介绍了创建和添加数据。除此，Backtrader 还提供了其他丰富的数据源类。\n大部分情况下，GenericCSVData 和 PandasData 已能满足需求。如有自定义数据列的需求，Backtrader 也支持自定义数据。\n添加数据部分，除了 cerebro.adddata 直接添加，还可以重放、重采样数据，模拟实际交易环境。\n下一节，我们学习如何编写交易策略。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/03-datafeed/","section":"教程","summary":" 本文具体讲讲如何在 Backtrader 中配置数据源，也就是 DataFeed。\n我会先演示如何给 Backtrader 添加 CSV 数据源。然后，再介绍 Backtrader 中最常用的两种 DataFeed 的使用。\n","title":"数据源加载与配置","type":"docs"},{"content":"在某些情况下，由于资产操作涉及利率，经纪商的实际现金金额可能会减少。例如：\n股票卖空 ETF 的多头和空头操作 该费用直接从经纪账户的现金余额中扣除，但仍可视为佣金方案的一部分。因此，backtrader 对此进行了建模。\nCommInfoBase 类（以及主要的 CommissionInfo 接口对象）已扩展了以下两个新参数，用于设置利率并确定应用于空头还是多空双向：\n参数 # interest（默认值：0.0）\n如果非零，表示持有卖空头寸时收取的年度利息。主要用于股票卖空。\n默认公式：days * price * size * (interest / 365)\n必须以绝对值指定：0.05 -\u0026gt; 5%\n可以通过重写 get_credit_interest 方法来更改此行为。\ninterest_long（默认值：False）\n一些产品（如 ETF）对多头和空头头寸都收取利息。如果为 True 且 interest 非零，则两个方向都将收取利息。\n公式 # 默认实现将使用以下公式：\ndays * abs(size) * price * (interest / 365) 其中：\ndays：自头寸开立或上次利息计算以来经过的天数 重写公式 # 要更改公式，需要子类化 CommissionInfo。需要重写的方法是：\ndef _get_credit_interest(self, size, price, days, dt0, dt1): \u0026#39;\u0026#39;\u0026#39; 此方法返回经纪商收取的利息成本。 对于 ``size \u0026gt; 0`` 的情况，仅在类参数 ``interest_long`` 为 ``True`` 时调用此方法。 计算利率的公式为： 公式：``days * price * abs(size) * (interest / 365)`` 参数： - ``data``：收取利息的数据源 - ``size``：当前头寸大小。\u0026gt; 0 表示多头头寸，\u0026lt; 0 表示空头头寸（此参数不会为 ``0``） - ``price``：当前头寸价格 - ``days``：自上次利息计算以来经过的天数（这是（dt0 - dt1）.days） - ``dt0``：当前日期时间（datetime.datetime） - ``dt1``：上次计算日期时间（datetime.datetime） ``dt0`` 和 ``dt1`` 在默认实现中未使用，并作为重写方法的额外输入提供 \u0026#39;\u0026#39;\u0026#39; 如果经纪商在计算利息时不考虑周末或银行假日，可以这样实现子类：\nimport backtrader as bt class MyCommissionInfo(bt.CommInfo): def _get_credit_interest(self, size, price, days, dt0, dt1): return 1.0 * abs(size) * price * (self.p.interest / 365.0) 这种情况下，公式中将：\ndays 替换为 1.0 因为如果周末/银行假日不计入，则每次计算都在上一次计算后的一个交易日进行。 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/11-commission-schemes/04-credit-interests/","section":"教程","summary":"在某些情况下，由于资产操作涉及利率，经纪商的实际现金金额可能会减少。例如：\n股票卖空 ETF 的多头和空头操作 该费用直接从经纪账户的现金余额中扣除，但仍可视为佣金方案的一部分。因此，backtrader 对此进行了建模。\n","title":"信用利息与融资融券","type":"docs"},{"content":"Fyne 项目分为许多包，每个包提供不同类型的功能，如下所示：\nfyne.io/fyne/v2 这个导入提供了所有 Fyne 代码共有的基本定义，包括数据类型和接口。 fyne.io/fyne/v2/app app 包提供启动新应用的 API。 通常你只需要 app.New() 或 app.NewWithID()。 fyne.io/fyne/v2/canvas canvas 包提供 Fyne 中所有的绘图 API。 完整的 Fyne 工具包由这些原始图形类型组成。 fyne.io/fyne/v2/container container 包提供用于布局和组织应用的容器。 fyne.io/fyne/v2/data/binding binding 包包含将数据源绑定到控件的方法。 fyne.io/fyne/v2/data/validation validation 包提供工具用于验证控件内的数据。 fyne.io/fyne/v2/dialog dialog 包包含确认、错误和文件保存/打开等对话框。 fyne.io/fyne/v2/layout layout 包提供用于容器的各种布局实现（在后续教程中讨论）。 fyne.io/fyne/v2/storage storage 包提供存储访问和管理功能。 fyne.io/fyne/v2/test 使用 test 包内的工具可以更容易地测试应用。 fyne.io/fyne/v2/widget 大多数图形应用是使用一系列控件创建的。 Fyne 中的所有控件和交互元素都在这个包中。 ","date":"2025-05-20","externalUrl":null,"permalink":"/docs/gofyne/09-architecture/04-organisation/","section":"教程","summary":"Fyne 项目分为许多包，每个包提供不同类型的功能，如下所示：\nfyne.io/fyne/v2 这个导入提供了所有 Fyne 代码共有的基本定义，包括数据类型和接口。 fyne.io/fyne/v2/app app 包提供启动新应用的 API。 通常你只需要 app.New() 或 app.NewWithID()。 fyne.io/fyne/v2/canvas canvas 包提供 Fyne 中所有的绘图 API。 完整的 Fyne 工具包由这些原始图形类型组成。 fyne.io/fyne/v2/container container 包提供用于布局和组织应用的容器。 fyne.io/fyne/v2/data/binding binding 包包含将数据源绑定到控件的方法。 fyne.io/fyne/v2/data/validation validation 包提供工具用于验证控件内的数据。 fyne.io/fyne/v2/dialog dialog 包包含确认、错误和文件保存/打开等对话框。 fyne.io/fyne/v2/layout layout 包提供用于容器的各种布局实现（在后续教程中讨论）。 fyne.io/fyne/v2/storage storage 包提供存储访问和管理功能。 fyne.io/fyne/v2/test 使用 test 包内的工具可以更容易地测试应用。 fyne.io/fyne/v2/widget 大多数图形应用是使用一系列控件创建的。 Fyne 中的所有控件和交互元素都在这个包中。 ","title":"包组织 Package","type":"docs"},{"content":"应用程序能够加载自定义主题，这些主题可以对标准主题进行小的更改，或提供完全独特的外观。一个主题需要实现fyne.Theme接口的函数，该接口定义如下：\ntype Theme interface { Color(ThemeColorName, ThemeVariant) color.Color Font(TextStyle) Resource Icon(ThemeIconName) Resource Size(ThemeSizeName) float32 } 要应用我们的主题更改，我们首先定义一个实现了这个接口的新类型。\n定义你的主题 # 我们从定义一个将成为我们主题的新类型开始，一个简单的空结构体就可以了：\ntype myTheme struct {} 断言我们实现了一个接口是个好主意，这样编译错误会更接近于定义类型。\nvar _ fyne.Theme = (*myTheme)(nil) 此时你可能会看到编译错误，因为我们还需要实现方法，我们从颜色开始。\n自定义颜色 # Theme接口中定义的Color函数要求我们定义一个命名颜色，并且还为用户期望的变体提供了提示（例如theme.VariantLight或theme.VariantDark）。在我们的主题中，我们将返回一个自定义的背景颜色 - 对于明亮和暗黑主题使用不同的值。\n// 需要从\u0026#34;image/color\u0026#34;导入color包。 func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { if name == theme.ColorNameBackground { if variant == theme.VariantLight { return color.White } return color.Black } return theme.DefaultTheme().Color(name, variant) } 你会看到这里的最后一行引用了theme.DefaultTheme()来查找标准值。这允许我们提供自定义值，但在我们不想提供自己的值时回退到标准主题。\n当然，颜色比资源更简单，我们来看看如何自定义图标。\n覆盖默认图标 # 图标（和字体）使用fyne.Resource作为值，而不是像int（用于大小）或color.Color（用于颜色）这样的简单类型。我们可以使用fyne.NewStaticResource构建自己的资源，或者你可以传入使用资源嵌入创建的值。\nfunc (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource { if name == theme.IconNameHome { return fyne.NewStaticResource(\u0026#34;myHome\u0026#34;, homeBytes) } return theme.DefaultTheme().Icon(name) } 如上所述，如果我们不想提供特定的覆盖，我们返回默认主题图标。\n加载主题 # 在你可以加载主题之前，你还需要实现Size和Font方法。如果你满意使用默认值，你可以使用这些空实现。\nfunc (m myTheme) Font(style fyne.TextStyle) fyne.Resource { return theme.DefaultTheme().Font(style) } func (m myTheme) Size(name fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(name) } 要为你的应用设置主题，你需要添加以下代码行：\napp.Settings().SetTheme(\u0026amp;myTheme{}) 通过这些更改，你可以应用自己的风格，进行小的调整或提供完全自定义的应用程序外观！\n","date":"2025-04-29","externalUrl":null,"permalink":"/docs/gofyne/08-extend/04-custom-theme/","section":"教程","summary":"应用程序能够加载自定义主题，这些主题可以对标准主题进行小的更改，或提供完全独特的外观。一个主题需要实现fyne.Theme接口的函数，该接口定义如下：\n","title":"自定义主题 Theme","type":"docs"},{"content":"到目前为止，我们使用的数据绑定是数据类型与输出类型匹配的情况（例如String与Label或Entry）。通常，将需要以不同于原始格式的方式展示数据。为此，binding包提供了许多有用的转换函数。\n最常见的用途是将不同类型的数据转换为字符串，以便在Label或Entry控件中显示。看看我们如何使用binding.FloatToString将Float转换为String。原始值可以通过移动滑块来编辑。每次数据变化时，它都会运行转换代码并更新任何连接的控件。\n也可以使用格式字符串为用户界面添加更自然的输出。你可以看到我们的short绑定也是将Float转换为String，但通过使用WithFormat助手，我们可以传递一个格式字符串（类似于fmt包）来提供自定义输出。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/data/binding\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;转换\u0026#34;) f := binding.NewFloat() str := binding.FloatToString(f) short := binding.FloatToStringWithFormat(f, \u0026#34;%0.0f%%\u0026#34;) f.Set(25.0) w.SetContent(container.NewVBox( widget.NewSliderWithData(0, 100.0, f), widget.NewLabelWithData(str), widget.NewLabelWithData(short), )) w.ShowAndRun() } 最后，在本节中，我们将查看list数据。\n","date":"2025-04-11","externalUrl":null,"permalink":"/docs/gofyne/07-binding/03-conversion/","section":"教程","summary":"到目前为止，我们使用的数据绑定是数据类型与输出类型匹配的情况（例如String与Label或Entry）。通常，将需要以不同于原始格式的方式展示数据。为此，binding包提供了许多有用的转换函数。\n","title":"数据转换","type":"docs"},{"content":"有各种控件可用于向用户展示选择，包括复选框、单选按钮组和下拉选择框。\nwidget.Check 提供一个简单的是/否选择，使用字符串标签创建。这些控件每一个都接受一个 \u0026ldquo;changed\u0026rdquo; func(...)，其中参数类型适用于它们。因此，widget.NewCheck(..) 接受一个 string 参数作为标签和一个 func(bool) 参数作为更改处理器。你也可以使用 Checked 字段来获取布尔值。\n单选按钮控件类似，但第一个参数是表示每个选项的 string 切片。这次更改函数期望一个 string 参数，以返回当前选定的值。调用 widget.NewRadioGroup(...) 来构造单选按钮组控件，你可以稍后使用这个引用来读取 Selected 字段，而不是使用更改回调。\n下拉选择控件在构造函数签名上与单选按钮控件相同。调用 widget.NewSelect(...) 将显示一个按钮，当点击时会显示一个弹出窗口，用户可以从中进行选择。对于长列表的选项，这更加合适。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;选择控件\u0026#34;) check := widget.NewCheck(\u0026#34;可选\u0026#34;, func(value bool) { log.Println(\u0026#34;复选框设置为\u0026#34;, value) }) radio := widget.NewRadioGroup([]string{\u0026#34;选项 1\u0026#34;, \u0026#34;选项 2\u0026#34;}, func(value string) { log.Println(\u0026#34;单选按钮设置为\u0026#34;, value) }) combo := widget.NewSelect([]string{\u0026#34;选项 1\u0026#34;, \u0026#34;选项 2\u0026#34;}, func(value string) { log.Println(\u0026#34;选择设置为\u0026#34;, value) }) myWindow.SetContent(container.NewVBox(check, radio, combo)) myWindow.ShowAndRun() } ","date":"2025-03-06","externalUrl":null,"permalink":"/docs/gofyne/05-widget/04-choices/","section":"教程","summary":"有各种控件可用于向用户展示选择，包括复选框、单选按钮组和下拉选择框。\nwidget.Check 提供一个简单的是/否选择，使用字符串标签创建。这些控件每一个都接受一个 “changed” func(...)，其中参数类型适用于它们。因此，widget.NewCheck(..) 接受一个 string 参数作为标签和一个 func(bool) 参数作为更改处理器。你也可以使用 Checked 字段来获取布尔值。\n","title":"复选框 Choices","type":"docs"},{"content":"边框布局可能是构建用户界面时使用最广泛的布局之一，因为它允许围绕一个将扩展以填充空间的中心元素定位项目。要创建一个边框容器，你需要将应该在边框位置定位的 fyne.CanvasObject 作为构造函数的前四个参数传递。这个语法基本上就是 container.NewBorder(top, bottom, left, right, center)，如示例所示。\n传递给容器的前四个项目之后的任何项目都将被定位到中心区域，并将扩展以填充可用空间。你也可以将 nil 传递给你希望留空的边框参数。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/layout\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;边框布局\u0026#34;) top := canvas.NewText(\u0026#34;顶部栏\u0026#34;, color.White) left := canvas.NewText(\u0026#34;左侧\u0026#34;, color.White) middle := canvas.NewText(\u0026#34;内容\u0026#34;, color.White) content := container.NewBorder(top, nil, left, nil, middle) myWindow.SetContent(content) myWindow.ShowAndRun() } 请注意，中心的所有项目都将扩展以填充空间（就像它们在 layout.MaxLayout 容器中一样）。要自己管理该区域，你可以使用任何 fyne.Container 作为内容。\n","date":"2025-02-07","externalUrl":null,"permalink":"/docs/gofyne/04-container/04-border/","section":"教程","summary":"边框布局可能是构建用户界面时使用最广泛的布局之一，因为它允许围绕一个将扩展以填充空间的中心元素定位项目。要创建一个边框容器，你需要将应该在边框位置定位的 fyne.CanvasObject 作为构造函数的前四个参数传递。这个语法基本上就是 container.NewBorder(top, bottom, left, right, center)，如示例所示。\n","title":"边框布局 Border","type":"docs"},{"content":"","date":"2025-01-26","externalUrl":null,"permalink":"/docs/gofyne/04-container/","section":"教程","summary":"","title":"容器与布局","type":"docs"},{"content":"canvas.Circle 定义了一个由指定颜色填充的圆形。您还可以设置 StrokeWidth，因此显示不同的 StrokeColor，如此示例中所示。\n圆形将填充通过调用 Resize() 或由其控制的布局指定的空间。由于示例将圆形设置为窗口内容，它将调整大小以填充窗口，存在基本的内边距（由主题控制）。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;圆形\u0026#34;) circle := canvas.NewCircle(color.White) circle.StrokeColor = color.Gray{Y: 0x99} circle.StrokeWidth = 5 w.SetContent(circle) w.Resize(fyne.NewSize(100, 100)) w.ShowAndRun() } 所有这些都是基本类型，可以由我们的驱动程序渲染，无需额外信息。接下来，我们将看看更复杂的类型，从 Image 开始。\n","date":"2025-01-11","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/04-circle/","section":"教程","summary":"canvas.Circle 定义了一个由指定颜色填充的圆形。您还可以设置 StrokeWidth，因此显示不同的 StrokeColor，如此示例中所示。\n圆形将填充通过调用 Resize() 或由其控制的布局指定的空间。由于示例将圆形设置为窗口内容，它将调整大小以填充窗口，存在基本的内边距（由主题控制）。\n","title":"圆 Circle","type":"docs"},{"content":" 水平盒（HBox） # 水平盒将项目以水平行的方式排列。容器中的每个元素将具有相同的高度（容器中最高项目的高度），并且对象将按其最小宽度左对齐。\n垂直盒（VBox） # 垂直盒将项目以垂直列的方式排列。容器中的每个元素将具有相同的宽度（容器中最宽项目的宽度），并且对象将按其最小高度顶部对齐。\n居中（Center） # 居中布局将所有容器元素定位在容器的中心。每个对象都将设置为其最小尺寸。\n表单（Form） # 表单布局将项目成对排列，其中第一列为最小宽度。这通常用于表单中的标签元素，其中标签位于第一列，其描述的项目位于第二列。你应该总是向表单布局添加偶数个元素。\n网格（Grid） # 网格布局在可用空间中均匀排列项目。指定列数，对象水平定位，直到达到列数，此时开始新的行。所有对象具有相同的大小，即宽度除以列总数，高度将是总高度除以所需行数减去填充。\n网格包裹（GridWrap） # 网格包裹布局将所有项目沿一行流动排列，如果空间不足，则换到新行。所有对象将设置为相同的大小，即传递给布局的大小。这种布局可能不会尊重项目的MinSize以管理这种统一布局。通常用于文件管理器或图像缩略图列表。\n边框（Border） # 边框布局支持在可用空间外围定位项目。边框传递指向对象的指针（顶部、左侧、底部、右侧）。容器中未定位在边框上的所有项目将填充剩余空间。\n最大（Max） # 最大布局将所有容器元素定位以填充可用空间。对象都将是全尺寸的，并按照它们被添加到容器中的顺序绘制（最后添加的在最上面）。\n填充（Padded） # 填充布局将所有容器元素定位以填充可用空间，但在外围有一小块填充。填充的大小是主题特定的。对象都将按照它们被添加到容器中的顺序绘制（最后添加的在最上面）。\n组合布局 # 通过使用多个布局，可以构建更复杂的应用程序结构。每个具有自己布局的多个容器可以嵌套，仅使用上面列出的标准布局创建完整的用户界面布局。例如，一个用于页眉的水平盒，一个用于左侧文件面板的垂直盒，以及内容区域中的网格包装布局 - 所有这些都放在使用边框布局的容器内，可以构建下面所示的结果。\n","date":"2024-12-06","externalUrl":null,"permalink":"/docs/gofyne/02-explore/04-layouts/","section":"教程","summary":"水平盒（HBox） # 水平盒将项目以水平行的方式排列。容器中的每个元素将具有相同的高度（容器中最高项目的高度），并且对象将按其最小宽度左对齐。\n","title":"布局组件","type":"docs"},{"content":"对于一个图形用户界面（GUI）应用程序来说，它需要运行一个事件循环（有时被称为运行循环），来处理用户交互和绘图事件。在Fyne中，这是通过使用App.Run()或Window.ShowAndRun()函数启动的。这些函数中的一个必须在你的main()函数的设置代码末尾被调用。\n一个应用程序应该只有一个运行循环，因此你应该在代码中只调用Run()一次。第二次调用它将会导致错误。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Hello\u0026#34;) myWindow.SetContent(widget.NewLabel(\u0026#34;Hello\u0026#34;)) myWindow.Show() myApp.Run() tidyUp() } func tidyUp() { fmt.Println(\u0026#34;Exited\u0026#34;) } 对于桌面运行时，一个应用程序可以通过调用App.Quit()直接退出（移动应用不支持此功能）- 通常在开发者代码中不需要。一旦所有窗口都被关闭，应用程序也将退出。另外，请注意，在应用程序退出之前，执行Run()之后的函数将不会被调用。\n","date":"2024-10-25","externalUrl":null,"permalink":"/docs/gofyne/01-started/04-apprun/","section":"教程","summary":"对于一个图形用户界面（GUI）应用程序来说，它需要运行一个事件循环（有时被称为运行循环），来处理用户交互和绘图事件。在Fyne中，这是通过使用App.Run()或Window.ShowAndRun()函数启动的。这些函数中的一个必须在你的main()函数的设置代码末尾被调用。\n","title":"App 和 RunLoop","type":"docs"},{"content":"我们再来了解 4 个非 oh-my-zsh 内置插件，它们分别是 zsh-syntax-highlighting、zsh-autosuggestions、zsh-history-substring-search 和 you-should-use。这些插件由 zsh 社区开发。\n开始介绍前，先将它们全部安装配置完成。\n安装 # 这 4 个插件需要手动从 GitHub 克隆到 ~/.oh-my-zsh/custom/plugins/ 目录：\n# 语法高亮 git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \\ ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting # 自动建议 git clone https://github.com/zsh-users/zsh-autosuggestions.git \\ ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions # 历史命令搜索 git clone https://github.com/zsh-users/zsh-history-substring-search.git \\ ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-history-substring-search # 别名提示 git clone https://github.com/MichaelAquilina/zsh-you-should-use.git \\ ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/you-should-use 然后在 ~/.zshrc 的 plugins 中加入：\nplugins=( ... # 之前的基础插件 zsh-syntax-highlighting zsh-autosuggestions zsh-history-substring-search you-should-use ) 执行 source ~/.zshrc 生效。\nzsh-syntax-highlighting # 必装！ 这个插件会在你输入命令的同时进行语法高亮：\n合法命令 → 绿色 非法命令 → 红色 存在的路径 → 带下划线 选项 → 紫色 输入 docker ps，docker 变绿；输入 docker p 还没敲完时灰色，敲完后变绿——即时反馈，减少低级错误。\nzsh-autosuggestions # 同样必装！ 它会根据你的历史记录，在输入时以灰色提示你可能想打的完整命令，按 → 或 Ctrl + F 直接补全：\n$ docker compose up -d # 之前执行过的命令 $ doc # 输入时，灰色显示 →ker compose up -d # 按 → 自动补全 用久了你会发现，日常 80% 的命令都是重复的—— git push、docker compose up、ssh server——这个插件让你几乎不用完整打字。\nzsh-history-substring-search # oh-my-zsh 自带的 history 插件需要输入关键字再按方向键向上搜索。而这个插件更自然：\n快捷键 动作 Ctrl + P / ↑ 向上匹配历史 Ctrl + N / ↓ 向下匹配历史 输入 git p 然后按 ↑，它会依次匹配 git push、git pull、git log -p……而且高亮匹配的关键字部分，方便定位。\nyou-should-use # 这个插件有点意思——它会在你输入一个命令的完整形式时，提示你还有别名可用：\n$ git status # You should use: gst $ docker ps # You should use: dps （如果有配置 docker 插件别名） 它是学习 aliases 的绝佳工具——安装后正常用原生命令，插件会提醒你有哪些更短的别名可用，久而久之就自然记住了。\n小结 # 这 4 个插件配合使用，体验提升非常明显：\n语法高亮 → 输错命令立刻知道 自动建议 → 重复命令不用再打 历史搜索 → 往事如风，上下翻一翻就回来了 别名提示 → 被动学习最有效率 安装完它们，终端的智能程度已经远超默认 bash 了。\n","date":"2024-03-10","externalUrl":null,"permalink":"/docs/mytermenv/ohmyzsh/advancedplugins/","section":"教程","summary":"我们再来了解 4 个非 oh-my-zsh 内置插件，它们分别是 zsh-syntax-highlighting、zsh-autosuggestions、zsh-history-substring-search 和 you-should-use。这些插件由 zsh 社区开发。\n","title":"高级插件","type":"docs"},{"content":"这部分主要介绍 iTerm2 提供的 Python API，利用它，带你实现一些不一样的能力。我将演示两个案例，分别是背景图自动更换和分屏创建自动化。\n环境准备 # iTerm2 的 Python API 需要先启用脚本支持。打开 Preferences \u0026gt; General \u0026gt; Magic \u0026gt; Enable Python API。\n安装依赖：\n# 安装 iterm2 Python 包 pip install iterm2 iTerm2 的 Python API 通过 WebSocket 与 iTerm2 进程通信，脚本可以控制 iTerm2 的几乎所有方面——窗口、标签页、分屏、配色、文本内容等。\n案例一：自动更换背景图 # 每天换个背景图保持新鲜感。这个脚本每天自动更换 iTerm2 的背景图片。\n#!/usr/bin/env python3 import iterm2 import random from pathlib import Path WALLPAPER_DIR = Path.home() / \u0026#34;Pictures\u0026#34; / \u0026#34;iterm2-wallpapers\u0026#34; async def main(connection): # 获取所有 session app = await iterm2.async_get_app(connection) # 列出所有图片文件 images = list(WALLPAPER_DIR.glob(\u0026#34;*.{jpg,png,jpeg,webp}\u0026#34;)) if not images: print(f\u0026#34;未在 {WALLPAPER_DIR} 中找到图片\u0026#34;) return # 随机选一张 wallpaper = random.choice(images) # 应用到所有 profile for profile in await iterm2Profile.async_query(connection): change = iterm2.LocalWriteOnlyProfile() change.set_background_image_location(str(wallpaper)) await profile.async_set_local_profile(connection, change) print(f\u0026#34;背景图已更换为: {wallpaper.name}\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: iterm2.run_until_complete(main) 配合 cron 或 launchd 每天定时执行即可。\n案例二：分屏创建自动化 # 如果你经常需要固定的开发布局——比如左边 2/3 编辑器，右边上下两个终端——可以写个脚本一键创建：\n#!/usr/bin/env python3 import iterm2 async def main(connection): app = await iterm2.async_get_app(connection) # 获取当前窗口 window = app.current_terminal_window if window is None: print(\u0026#34;没有打开的 iTerm2 窗口\u0026#34;) return # 清除当前标签页的分屏 session = window.current_tab.current_session # 垂直分屏（右边 1/3） right_session = await session.async_split_pane( vertical=True, before=False, profile=\u0026#34;Default\u0026#34; ) # 右边再水平分成上下两个 await right_session.async_split_pane( vertical=False, before=False, profile=\u0026#34;SSH\u0026#34; ) # 左边打开编辑器 await session.async_send_text(\u0026#34;vim\\n\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: iterm2.run_until_complete(main) 运行脚本：\npython3 layout_dev.py 你就可以一键创建一个标准开发布局：左侧 Vim，右侧上下一分为二。\n更多可能性 # iTerm2 的 Python API 还能做很多事情：\n根据当前 SSH 的目标服务器自动切换配色 定时轮换主题配色 监控终端活动并在指定条件时弹窗提醒 集成 CI/CD 流水线，在终端显示构建状态 文档参考：iTerm2 Python API Documentation\n小结 # iTerm2 的 Python API 为终端自动化提供了无限可能。从简单的背景图轮换到复杂的布局管理，只要你能想到的终端操作，基本上都能通过脚本实现自动化。\n","date":"2024-01-28","externalUrl":null,"permalink":"/docs/mytermenv/terminal/pyapi/","section":"教程","summary":"这部分主要介绍 iTerm2 提供的 Python API，利用它，带你实现一些不一样的能力。我将演示两个案例，分别是背景图自动更换和分屏创建自动化。\n环境准备 # iTerm2 的 Python API 需要先启用脚本支持。打开 Preferences \u003e General \u003e Magic \u003e Enable Python API。\n","title":"Python API","type":"docs"},{"content":" 高效 Shell 命令 # 类 Unix 系统发展多年，不少古董命令还在占据终端的绝大部分时间，但它们的使用体验上却是差强人意。最能说明问题的就是那个 cd 命令，无论是多么丝滑的操作，一旦遇到需要 change directory 就会变得磕磕绊绊。\n本章节，我计划介绍提升终端效率的一系列命令，它们更具现代风格，希望能让你眼前一亮。\n","externalUrl":null,"permalink":"/docs/mytermenv/commands/","section":"教程","summary":"高效 Shell 命令 # 类 Unix 系统发展多年，不少古董命令还在占据终端的绝大部分时间，但它们的使用体验上却是差强人意。最能说明问题的就是那个 cd 命令，无论是多么丝滑的操作，一旦遇到需要 change directory 就会变得磕磕绊绊。\n","title":"高效命令","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/","section":"教程","summary":"","title":"核心概念","type":"docs"},{"content":"1.9.37.116 版本增加了 Bracket 订单，为回测 Broker 提供了广泛的订单支持（Market、Limit、Close、Stop、StopLimit、StopTrail、StopTrailLimit、OCO）。\n注意，目前已在回测和 Interactive Brokers 中实现。\nBracket 订单不是单个订单，而是由 3 个订单组成。\n以做多为例：\n主要买单，通常设置为 Limit 或 StopLimit 订单。 低价卖单，通常设置为 Stop 订单以限制损失。 高价卖单，通常设置为 Limit 订单以获取利润。 做空则相反：一个主要卖单加 2 个买单。\n低价/高价卖单围绕主要订单形成一个 Bracket。\n规则如下：\n3 个订单同时提交，避免任何订单独立触发。 低价/高价卖单被标记为主要订单的子订单。 子订单在主要订单执行前不活跃。 取消主要订单会同时取消低价和高价卖单。 执行主要订单会激活低价和高价卖单。 激活后，低价/高价卖单中任一执行或取消，会自动取消另一个。 使用模式 # 有两种方式创建 Bracket 订单组：\n单次发布3个订单。 手动发布3个订单。 单次发布 Bracket # backtrader 在 Strategy 中提供了两个新方法：buy_bracket 和 sell_bracket。\n注意：签名和信息见下文或 Strategy 参考部分。\n通过单个语句完成 3 个订单的设置，示例如下：\nbrackets = self.buy_bracket(limitprice=14.00, price=13.50, stopprice=13.00) 注意，stopprice和limitprice围绕price设定。这应该足够了。\n目标数据为 data0，大小由默认 sizer 自动确定。也可以指定其他参数来精细控制执行。\n返回值是一个包含3个订单的列表：[主要订单，stop订单，limit订单]。\n因为发布 sell_bracket 时低价和高价会翻转，参数命名约定：stop 用于止损（做多时是低价，做空时是高价），limit 用于获利（做多时是高价，做空时是低价）。\n手动发布Bracket # 手动方式需要生成 3 个订单，并处理 transmit 和 parent 参数。规则如下：\n先创建主要订单，设置 transmit=False。 低价/高价订单必须指定 parent=main_side_order。 第一个创建的低价/高价订单设置 transmit=False。 最后一个创建的订单（低价或高价）设置 transmit=True。 以下示例实现了与上述单次命令相同的效果：\nmainside = self.buy(price=13.50, exectype=bt.Order.Limit, transmit=False) lowside = self.sell(price=13.00, size=mainside.size, exectype=bt.Order.Stop, transmit=False, parent=mainside) highside = self.sell(price=14.00, size=mainside.size, exectype=bt.Order.Limit, transmit=True, parent=mainside) 需要额外处理：\n让主要订单成为其他订单的父订单。 控制 transmit，确保只有最后一个订单触发提交。 指定执行类型。 为低价和高价订单指定大小。 三个订单的大小必须相同。如果未手动指定，sizer 可能为不同订单计算不同的大小，因此需要在调用时手动指定。\n示例 # 运行下面的示例生成如下输出（为简洁起见进行了截断）：\n$ ./bracket.py --plot 2005-01-28: Oref 1 / Buy at 2941.11055 2005-01-28: Oref 2 / Sell Stop at 2881.99275 2005-01-28: Oref 3 / Sell Limit at 3000.22835 2005-01-31: Order ref: 1 / Type Buy / Status Submitted 2005-01-31: Order ref: 2 / Type Sell / Status Submitted 2005-01-31: Order ref: 3 / Type Sell / Status Submitted 2005-01-31: Order ref: 1 / Type Buy / Status Accepted 2005-01-31: Order ref: 2 / Type Sell / Status Accepted 2005-01-31: Order ref: 3 / Type Sell / Status Accepted 2005-02-01: Order ref: 1 / Type Buy / Status Expired 2005-02-01: Order ref: 2 / Type Sell / Status Canceled 2005-02-01: Order ref: 3 / Type Sell / Status Canceled ... 2005-08-11: Oref 16 / Buy at 3337.3892 2005-08-11: Oref 17 / Sell Stop at 3270.306 2005-08-11: Oref 18 / Sell Limit at 3404.4724 2005-08-12: Order ref: 16 / Type Buy / Status Submitted 2005-08-12: Order ref: 17 / Type Sell / Status Submitted 2005-08-12: Order ref: 18 / Type Sell / Status Submitted 2005-08-12: Order ref: 16 / Type Buy / Status Accepted 2005-08-12: Order ref: 17 / Type Sell / Status Accepted 2005-08-12: Order ref: 18 / Type Sell / Status Accepted 2005-08-12: Order ref: 16 / Type Buy / Status Completed 2005-08-18: Order ref: 17 / Type Sell / Status Completed 2005-08-18: Order ref: 18 / Type Sell / Status Canceled ... 2005-09-26: Oref 22 / Buy at 3383.92535 2005-09-26: Oref 23 / Sell Stop at 3315.90675 2005-09-26: Oref 24 / Sell Limit at 3451.94395 2005-09-27: Order ref: 22 / Type Buy / Status Submitted 2005-09-27: Order ref: 23 / Type Sell / Status Submitted 2005-09-27: Order ref: 24 / Type Sell / Status Submitted 2005-09-27: Order ref: 22 / Type Buy / Status Accepted 2005-09-27: Order ref: 23 / Type Sell / Status Accepted 2005-09-27: Order ref: 24 / Type Sell / Status Accepted 2005-09-27: Order ref: 22 / Type Buy / Status Completed 2005-10-04: Order ref: 24 / Type Sell / Status Completed 2005-10-04: Order ref: 23 / Type Sell / Status Canceled ... 展示了 3 种不同结果：\n主要订单过期，自动取消其他两个订单。 主要订单完成，低价止损订单执行，限制了损失。 主要订单完成，高价限价订单执行。 注意，完成的订单 id 是 22 和 24（高价订单最后发布），未执行的低价订单 id 是 23。\n图示\n可以看出亏损和盈利交易分别集中在相近的价位，这正是 Bracket 的目的——控制两侧风险。\n运行的示例手动发布3个订单，但可以使用buy_bracket。输出如下：\n$ ./bracket.py --strat usebracket=True 结果相同。\n参考 # 请参阅新的 buy_bracket 和 sell_bracket 方法\ndef buy_bracket(self, data=None, size=None, price=None, plimit=None, exectype=bt.Order.Limit, valid=None, tradeid=0, trailamount=None, trailpercent=None, oargs={}, stopprice=None, stopexec=bt.Order.Stop, stopargs={}, limitprice=None, limitexec=bt.Order.Limit, limitargs={}, **kwargs): \u0026#39;\u0026#39;\u0026#39; 创建一个Bracket订单组（低侧 - 买单 - 高侧）。默认行为如下： - 发出执行类型为“Limit”的**买单** - 发出执行类型为“Stop”的*低侧*Bracket**卖单** - 发出执行类型为“Limit”的*高侧*Bracket**卖单**。 参见下文以了解不同参数的含义 - ``data``（默认：``None``） 订单针对的数据。如果为``None``，则使用系统中的第一个数据，即``self.datas[0 ]或self.data0``（即``self.data``） - ``size``（默认：``None``） 订单的数据单位大小（正数）。 如果为``None``，则使用通过``getsizer``检索到的``sizer``实例来确定大小。 **注意**：相同的大小适用于Bracket的所有3个订单 - ``price``（默认：``None``） 使用的价格（实时经纪商可能会对实际格式施加限制，如果不符合最小价格单位要求） ``None``对于``Market``和``Close``订单是有效的（市场决定价格） 对于``Limit``、``Stop``和``StopLimit``订单，此值确定触发点（在``Limit``的情况下，触发点显然是订单应匹配的价格） - ``plimit``（默认：``None``） 仅适用于``StopLimit``订单。这是设置隐含限价订单的价格，一旦触发了``Stop``（使用``price``） - ``trailamount``（默认：``None``） 如果订单类型为StopTrail或StopTrailLimit，这是一个绝对金额，确定价格的距离（卖单下方和买单上方），以保持追踪止损 - ``trailpercent``（默认：``None``） 如果订单类型为StopTrail或StopTrailLimit，这是一个百分比金额，确定价格的距离（卖单下方和买单上方），以保持追踪止损（如果也指定了``trailamount``，将使用它） - ``exectype``（默认：``bt.Order.Limit``） 可能的值：（参见``buy``方法的文档） - ``valid``（默认：``None``） 可能的值：（参见``buy``方法的文档） - ``tradeid``（默认：``0``） 可能的值：（参见``buy``方法的文档） - ``oargs``（默认：``{}``） 要传递给主要订单的特定关键字参数（在``dict``中）。默认``**kwargs``中的参数将应用于此。 - ``**kwargs``：其他经纪商实现可能支持的额外参数。``backtrader``将*kwargs*传递给创建的订单对象 可能的值：（参见``buy``方法的文档） **注意**：此``kwargs``将应用于Bracket的所有3个订单。参见下文以了解低侧和高侧订单的特定关键字参数 - ``stopprice``（默认：``None``） *低侧*止损订单的特定价格 - ``stopexec``（默认：``bt.Order.Stop``） *低侧*订单的特定执行类型 - ``stopargs``（默认：``{}``） 要传递给低侧订单的特定关键字参数（在``dict``中）。默认``**kwargs``中的参数将应用于此。 - ``limitprice``（默认：``None``） *高侧*止损订单的特定价格 - ``stopexec``（默认：``bt.Order.Limit``） *高侧*订单的特定执行类型 - ``limitargs``（默认：``{}``） 要传递给高侧订单的特定关键字参数（在``dict``中）。默认``**kwargs``中的参数将应用于此。 返回： - 一个包含3个Bracket订单的列表[订单，低侧，高侧] \u0026#39;\u0026#39;\u0026#39; def sell_bracket(self, data=None, size=None, price=None, plimit=None, exectype=bt.Order.Limit, valid=None, tradeid=0, trailamount=None, trailpercent=None, oargs={}, stopprice=None, stopexec=bt.Order.Stop, stopargs={}, limitprice=None, limitexec=bt.Order.Limit, limitargs={}, **kwargs): \u0026#39;\u0026#39;\u0026#39; 创建一个Bracket订单组（低侧 - 卖单 - 高侧）。默认行为如下： - 发出执行类型为“Limit”的**卖单** - 发出执行类型为“Stop”的*高侧*Bracket**买单** - 发出执行类型为“Limit”的*低侧*Bracket**买单**。 参见``bracket_buy``以了解参数的含义 返回： - 一个包含3个Bracket订单的列表[订单，低侧，限价侧] \u0026#39;\u0026#39;\u0026#39; 示例用法 # $ ./bracket.py --help usage: bracket.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] Sample Skeleton optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) 示例代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class St(bt.Strategy): params = dict( ma=bt.ind.SMA, p1=5, p2=15, limit=0.005, limdays=3, limdays2=1000, hold=10, usebracket=False, # use order_target_size switchp1p2=False, # switch prices of order1 and order2 ) def notify_order(self, order): print(\u0026#39;{}: Order ref: {} / Type {} / Status {}\u0026#39;.format( self.data.datetime.date(0), order.ref, \u0026#39;Buy\u0026#39; * order.isbuy() or \u0026#39;Sell\u0026#39;, order.getstatusname())) if order.status == order.Completed: self.holdstart = len(self) if not order.alive() and order.ref in self.orefs: self.orefs.remove(order.ref) def __init__(self): ma1, ma2 = self.p.ma(period=self.p.p1), self.p.ma(period=self.p.p2) self.cross = bt.ind.CrossOver(ma1, ma2) self.orefs = list() if self.p.usebracket: print(\u0026#39;-\u0026#39; * 5, \u0026#39;Using buy_bracket\u0026#39;) def next(self): if self.orefs: return # pending orders do nothing if not self.position: if self.cross \u0026gt; 0.0: # crossing up close = self.data.close[0] p1 = close * (1.0 - self.p.limit) p2 = p1 - 0.02 * close p3 = p1 + 0.02 * close valid1 = datetime.timedelta(self.p.limdays) valid2 = valid3 = datetime.timedelta(self.p.limdays2) if self.p.switchp1p2: p1, p2 = p2, p1 valid1, valid2 = valid2, valid1 if not self.p.usebracket: o1 = self.buy(exectype=bt.Order.Limit, price=p1, valid=valid1, transmit=False) print(\u0026#39;{}: Oref {} / Buy at {}\u0026#39;.format(self.datetime.date(), o1.ref, p1)) o2 = self.sell(exectype=bt.Order.Stop, price=p2, valid=valid2, parent=o1, transmit=False) print(\u0026#39;{}: Oref {} / Sell Stop at {}\u0026#39;.format(self.datetime.date(), o2.ref, p2)) o3 = self.sell(exectype=bt.Order.Limit, price=p3, valid=valid3, parent=o1, transmit=True) print(\u0026#39;{}: Oref {} / Sell Limit at {}\u0026#39;.format(self.datetime.date(), o3.ref, p3)) self.orefs = [o1.ref, o2.ref, o3.ref] else: os = self.buy_bracket(price=p1, valid=valid1, stopprice=p2, stopargs=dict(valid=valid2), limitprice=p3, limitargs=dict(valid=valid3),) self.orefs = [o.ref for o in os] else: # in the market if (len(self) - self.holdstart) \u0026gt;= self.p.hold: pass # do nothing in this case def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # Data feed kwargs kwargs = dict() dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) cerebro.run() if args.plot: cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Sample Skeleton\u0026#39; ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/09-orders/05-bracket-orders/","section":"教程","summary":"1.9.37.116 版本增加了 Bracket 订单，为回测 Broker 提供了广泛的订单支持（Market、Limit、Close、Stop、StopLimit、StopTrail、StopTrailLimit、OCO）。\n","title":"Bracket 止损止盈订单","type":"docs"},{"content":"在策略中检查资产的头寸（Position），可通过 position 属性或 getposition(data=None, broker=None) 方法。这会返回策略在 Cerebro 默认 Broker 中 datas[0] 的头寸。\n头寸仅表示：\n持有数量（size） 平均价格（price） 它用作状态指示，例如决定是否需要发出订单（如仅在没有持仓时开仓）。\n参考 # class backtrader.position.Position(size=0, price=0.0) 保存并更新头寸的数量和价格。该对象与任何资产没有关系。它只保存数量和价格。\n成员属性：\nsize（int）：当前头寸的数量 price（float）：当前头寸的价格 可以使用 len(position) 来测试头寸实例以查看数量是否不为零。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/10-broker/05-position/","section":"教程","summary":"在策略中检查资产的头寸（Position），可通过 position 属性或 getposition(data=None, broker=None) 方法。这会返回策略在 Cerebro 默认 Broker 中 datas[0] 的头寸。\n头寸仅表示：\n持有数量（size） 平均价格（price） 它用作状态指示，例如决定是否需要发出订单（如仅在没有持仓时开仓）。\n","title":"Position 持仓管理","type":"docs"},{"content":"本文提出保守型公式的方法：Python 中的保守型公式：简化的量化投资\n这只是众多再平衡方法中的一种，相对易于理解。方法概要：\n从 Y 只股票中选出 x 只（如从 1000 只中选 100 只） 选股标准： 低波动性 高净派息收益率（Net Payout Yield，NPY） 高动量 每月再平衡一次 接下来展示如何在 Backtrader 中实现该策略。\n数据 # 即使有获胜的策略，没有数据一切也无从谈起。因此，需要考虑数据格式和加载方式。\n假设有一组 CSV 文件，每个文件包含：\n每月 OHLCV 数据 额外的净派息收益率（NPY）列，形成 ohlcvn 数据集 CSV 数据格式如下：\ndate, open, high, low, close, volume, npy 2001-12-31, 1.0, 1.0, 1.0, 1.0, 0.5, 3.0 2002-01-31, 2.0, 2.5, 1.1, 1.2, 3.0, 5.0 ... 每行表示一个月的数据。通过 Backtrader 的 CSV 数据加载引擎创建扩展类：\nclass NetPayOutData(bt.feeds.GenericCSVData): lines = (\u0026#39;npy\u0026#39;,) # 增加一行，用于存储净派息收益率 params = dict( npy=6, # npy字段位于第6列（基于0的索引） dtformat=\u0026#39;%Y-%m-%d\u0026#39;, # 设置日期格式为yyyy-mm-dd timeframe=bt.TimeFrame.Months, # 设置时间框架为按月 openinterest=-1, # -1表示没有openinterest字段 ) 这样就完成了数据源的扩展。通过 lines=('npy',)，将净派息收益率数据添加到了 OHLCV 数据流中。其他常见字段（如 open、high 等）已经是 GenericCSVData 的一部分。通过在 params 中指定位置，告知 Backtrader 净派息收益率所在的列。\n策略 # 将逻辑封装到 Backtrader 的标准策略中。为使其通用可自定义，采用与数据源相同的 params 方法。\n回顾快速总结中的要点：\n从一个包含 Y 只股票的股票池中选出 x 只 策略不负责将股票添加到池中，但负责选择。假设池中有 1000 只股票，设置了 x=100，但实际只加入了 50 只，策略仍会尝试选 100 只。为应对这种情况：\n设置 selperc 参数，默认 0.10（10%），即从池中选择该比例的股票。 例如，池中有 1000 只股票则选 100 只；只有 50 只则选 5 只。\n排名公式如下：\n(momentum * net payout) / volatility 即动量更大、派息收益率更高、波动性更低的股票评分更高。\n动量使用”变动率”指标（ROC，Rate of Change），衡量价格在一段时间内的变化比率。\n净派息收益率已作为数据的一部分包含在内。\n波动性使用基于 n 周期回报率的标准差计算。\n有了这些信息，策略可以初始化参数并设置每月迭代中使用的指标和计算方法。\n策略实现 # class St(bt.Strategy): params = dict( selcperc=0.10, # 从宇宙中选择的股票比例 rperiod=1, # 回报率计算周期，默认为1个周期 vperiod=36, # 波动性回顾期，默认为36个周期 mperiod=12, # 动量回顾期，默认为12个周期 reserve=0.05 # 5%的预留资本 ) def log(self, arg): print(\u0026#39;{} {}\u0026#39;.format(self.datetime.date(), arg)) def __init__(self): # 计算选股数量 self.selnum = int(len(self.datas) * self.p.selcperc) # 每只股票的资本分配比例 self.perctarget = (1.0 - self.p.reserve) / self.selnum # 计算回报率、波动性和动量 rs = [bt.ind.PctChange(d, period=self.p.rperiod) for d in self.datas] vs = [bt.ind.StdDev(ret, period=self.p.vperiod) for ret in rs] ms = [bt.ind.ROC(d, period=self.p.mperiod) for d in self.datas] # 排名公式： (动量 * 净派息收益率) / 波动性 self.ranks = {d: d.npy * m / v for d, v, m in zip(self.datas, vs, ms)} def next(self): # 按排名排序 ranks = sorted( self.ranks.items(), # 获取(d, rank)对 key=lambda x: x[1][0], # 使用排名（元素1）进行排序 reverse=True, # 按排名从高到低排序 ) # 获取排名前selnum的股票 rtop = dict(ranks[:self.selnum]) # 获取排名低的股票 rbot = dict(ranks[self.selnum:]) # 获取当前持有的股票 posdata = [d for d, pos in self.getpositions().items() if pos] # 卖出那些不再是前排名的股票 for d in (d for d in posdata if d not in rtop): self.log(\u0026#39;Exit {} - Rank {:.2f}\u0026#39;.format(d._name, rbot[d][0])) self.order_target_percent(d, target=0.0) # 重新平衡已经排名前的股票 for d in (d for d in posdata if d in rtop): self.log(\u0026#39;Rebal {} - Rank {:.2f}\u0026#39;.format(d._name, rtop[d][0])) self.order_target_percent(d, target=self.perctarget) del rtop[d] # 删除已处理的股票 # 为新进入的排名前的股票设置目标订单 for d in rtop: self.log(\u0026#39;Enter {} - Rank {:.2f}\u0026#39;.format(d._name, rtop[d][0])) self.order_target_percent(d, target=self.perctarget) 运行和评估 # 我们需要一些额外的代码来实现数据加载和运行策略的框架。\ndef run(args=None): args = parse_args(args) cerebro = bt.Cerebro() # 加载数据文件 for fname in glob.glob(os.path.join(args.datadir, \u0026#39;*\u0026#39;)): data = NetPayOutData(dataname=fname, **dkwargs) cerebro.adddata(data) # 添加策略 cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # 设置初始现金 cerebro.broker.setcash(args.cash) cerebro.run() # 执行策略 # 基本的性能评估 pnl = cerebro.broker.get_value() - args.cash print(\u0026#39;Profit ... or Loss: {:.2f}\u0026#39;.format(pnl)) 性能评估 # 简单的评估方法是计算最终资产值减去初始现金。\nBacktrader 还提供了内置的性能分析器，如 Sharpe 比率、加权回报率、SQN 等，可参考文档进一步分析。\n完整脚本 # import argparse import datetime import glob import os.path import backtrader as bt class NetPayOutData(bt.feeds.GenericCSVData): lines = (\u0026#39;npy\u0026#39;,) # add a line containing the net payout yield params = dict( npy=6, # npy field is in the 6th column (0 based index) dtformat=\u0026#39;%Y-%m-%d\u0026#39;, # fix date format a yyyy-mm-dd timeframe=bt.TimeFrame.Months, # fixed the timeframe openinterest=-1, # -1 indicates there is no openinterest field ) class St(bt.Strategy): params = dict( selcperc=0.10, # percentage of stocks to select from the universe rperiod=1, # period for the returns calculation, default 1 period vperiod=36, # lookback period for volatility - default 36 periods mperiod=12, # lookback period for momentum - default 12 periods reserve=0.05 # 5% reserve capital ) def log(self, arg): print(\u0026#39;{} {}\u0026#39;.format(self.datetime.date(), arg)) def __init__(self): # calculate 1st the amount of stocks that will be selected self.selnum = int(len(self.datas) * self.p.selcperc) # allocation perc per stock # reserve kept to make sure orders are not rejected due to # margin. Prices are calculated when known (close), but orders can only # be executed next day (opening price). Price can gap upwards self.perctarget = (1.0 - self.p.reserve) / self.selnum # returns, volatilities and momentums rs = [bt.ind.PctChange(d, period=self.p.rperiod) for d in self.datas] vs = [bt.ind.StdDev(ret, period=self.p.vperiod) for ret in rs] ms = [bt.ind.ROC(d, period=self.p.mperiod) for d in self.datas] # simple rank formula: (momentum * net payout) / volatility # the highest ranked: low vol, large momentum, large payout self.ranks = {d: d.npy * m / v for d, v, m in zip(self.datas, vs, ms)} def next(self): # sort data and current rank ranks = sorted( self.ranks.items(), # get the (d, rank), pair key=lambda x: x[1][0], # use rank (elem 1) and current time \u0026#34;0\u0026#34; reverse=True, # highest ranked 1st ... please ) # put top ranked in dict with data as key to test for presence rtop = dict(ranks[:self.selnum]) # For logging purposes of stocks leaving the portfolio rbot = dict(ranks[self.selnum:]) # prepare quick lookup list of stocks currently holding a position posdata = [d for d, pos in self.getpositions().items() if pos] # remove those no longer top ranked # do this first to issue sell orders and free cash for d in (d for d in posdata if d not in rtop): self.log(\u0026#39;Leave {} - Rank {:.2f}\u0026#39;.format(d._name, rbot[d][0])) self.order_target_percent(d, target=0.0) # rebalance those already top ranked and still there for d in (d for d in posdata if d in rtop): self.log(\u0026#39;Rebal {} - Rank {:.2f}\u0026#39;.format(d._name, rtop[d][0])) self.order_target_percent(d, target=self.perctarget) del rtop[d] # remove it, to simplify next iteration # issue a target order for the newly top ranked stocks # do this last, as this will generate buy orders consuming cash for d in rtop: self.log(\u0026#39;Enter {} - Rank {:.2f}\u0026#39;.format(d._name, rtop[d][0])) self.order_target_percent(d, target=self.perctarget) def run(args=None): args = parse_args(args) cerebro = bt.Cerebro() # Data feed kwargs dkwargs = dict(**eval(\u0026#39;dict(\u0026#39; + args.dargs + \u0026#39;)\u0026#39;)) # Parse from/to-date dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; if args.fromdate: fmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in args.fromdate) dkwargs[\u0026#39;fromdate\u0026#39;] = datetime.datetime.strptime(args.fromdate, fmt) if args.todate: fmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in args.todate) dkwargs[\u0026#39;todate\u0026#39;] = datetime.datetime.strptime(args.todate, fmt) # add all the data files available in the directory datadir for fname in glob.glob(os.path.join(args.datadir, \u0026#39;*\u0026#39;)): data = NetPayOutData(dataname=fname, **dkwargs) cerebro.adddata(data) # add strategy cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # set the cash cerebro.broker.setcash(args.cash) cerebro.run() # execute it all # Basic performance evaluation ... final value ... minus starting cash pnl = cerebro.broker.get_value() - args.cash print(\u0026#39;Profit ... or Loss: {:.2f}\u0026#39;.format(pnl)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=(\u0026#39;Rebalancing with the Conservative Formula\u0026#39;), ) parser.add_argument(\u0026#39;--datadir\u0026#39;, required=True, help=\u0026#39;Directory with data files\u0026#39;) parser.add_argument(\u0026#39;--dargs\u0026#39;, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in k1=v1,k2=v2 format\u0026#39;) # Defaults for dates parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in k1=v1,k2=v2 format\u0026#39;) parser.add_argument(\u0026#39;--cash\u0026#39;, default=1000000.0, type=float, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in k1=v1,k2=v2 format\u0026#39;) parser.add_argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in k1=v1,k2=v2 format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: run() 这个完整的脚本展示了如何在Backtrader中实现基于保守型公式的股票选择和再平衡策略。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/19-articles/05-rebalancing-conserative/","section":"教程","summary":"本文提出保守型公式的方法：Python 中的保守型公式：简化的量化投资\n这只是众多再平衡方法中的一种，相对易于理解。方法概要：\n从 Y 只股票中选出 x 只（如从 1000 只中选 100 只） 选股标准： 低波动性 高净派息收益率（Net Payout Yield，NPY） 高动量 每月再平衡一次 接下来展示如何在 Backtrader 中实现该策略。\n","title":"保守型公式再平衡策略","type":"docs"},{"content":"Backtrader 通过引入两个新的 Cerebro 参数，优化了在多进程环境下管理数据源和结果的方式。\n参数说明：\n参数名 默认值 描述 optdatas True 如果为True并进行优化（系统可以预加载并使用runonce），则数据预加载将仅在主进程中进行一次，以节省时间和资源。 optreturn True 如果为True，优化结果将不是完整的策略对象（包括所有数据、指标、观察者等），而是带有以下属性的对象（与策略中相同）：- params（或p）：策略执行时的参数- analyzers：策略执行的分析器 通常只需查看策略参数组合的最终表现（如收益率）。如需查看运行过程中的详细数据（如每个时间点的指标值），请关闭此选项。\n数据源管理 # 在优化场景中，Cerebro 参数可能的组合是：\npreload=True（默认）：数据源会在执行任何回测代码前完成预加载。 runonce=True（默认）：指标在紧凑的 for 循环中批量计算，而非逐步计算。 如果两个条件均为 True 且 optdatas=True，则数据源会在生成新子进程之前在主进程中预加载（子进程负责执行回测）。\n结果管理 # 在优化场景中，评估每次策略运行的不同参数时，最重要的两个因素是：\nstrategy.params（或 strategy.p）\n回测使用的实际参数集\nstrategy.analyzers\n提供策略实际表现评估的对象。例如：SharpeRatio_A（年化夏普比率）\n当 optreturn=True 时，不会返回完整的策略实例，而是创建占位符对象，仅携带上述两个属性供评估。\n这避免了传回大量生成的数据，例如回测期间指标生成的值。\n如需返回完整的策略对象，在实例化 Cerebro 或调用 cerebro.run 时设置 optreturn=False 即可。\n一些测试运行 # backtrader 源码中的优化示例已扩展并添加了对 optdatas 和 optreturn 的控制（实际上是禁用它们）。\n单核心运行 # 作为参考，当将CPU数量限制为1且不使用多进程模块时会发生什么：\n$ ./optimization.py --maxcpus 1 ================================================== ************************************************** -------------------------------------------------- OrderedDict([(u\u0026#39;smaperiod\u0026#39;, 10), (u\u0026#39;macdperiod1\u0026#39;, 12), (u\u0026#39;macdperiod2\u0026#39;, 26), (u\u0026#39;macdperiod3\u0026#39;, 9)]) ************************************************** -------------------------------------------------- OrderedDict([(u\u0026#39;smaperiod\u0026#39;, 10), (u\u0026#39;macdperiod1\u0026#39;, 13), (u\u0026#39;macdperiod2\u0026#39;, 26), (u\u0026#39;macdperiod3\u0026#39;, 9)]) ... ... OrderedDict([(u\u0026#39;smaperiod\u0026#39;, 29), (u\u0026#39;macdperiod1\u0026#39;, 19), (u\u0026#39;macdperiod2\u0026#39;, 29), (u\u0026#39;macdperiod3\u0026#39;, 14)]) ================================================== Time used: 184.922727833 多核心运行 # 在不限制 CPU 数量的情况下，Python 多进程模块会尝试使用所有 CPU。以下测试会禁用 optdatas 和 optreturn：\noptdatas和optreturn都启用\n默认行为：\n$ ./optimization.py ... ... ... ================================================== Time used: 56.5889185394 通过多核以及数据源和结果的优化，总时间从 184.92 秒减少到 56.58 秒。\n请注意，示例使用了252条数据，并且指标仅生成长度为252点的值。这只是一个例子。\n关键问题是这些改进有多少归功于新行为。\noptreturn 禁用：将完整的策略对象传回调用者：\n$ ./optimization.py --no-optreturn ... ... ... ================================================== Time used: 67.056914007 执行时间增加了 18.50%（速度提升 15.62%）。\noptdatas 禁用：每个子进程被迫加载各自的数据源值：\n$ ./optimization.py --no-optdatas ... ... ... ================================================== Time used: 72.7238112637 执行时间增加了 28.52%（速度提升 22.19%）。\n两者都禁用：仍然使用多核，但使用旧的未改进行为：\n$ ./optimization.py --no-optdatas --no-optreturn ... ... ... ================================================== Time used: 83.6246643786 执行时间增加了 47.79%（速度提升 32.34%）。\n这表明使用多个核心是时间改进的主要贡献因素。\n注意：这些测试是在配备 i7-4710HQ（4 核/8 逻辑线程）和 16 GB RAM 的笔记本电脑上进行的，操作系统为 Windows 10 64 位。其他环境下结果可能有所不同。\n总结 # 在优化过程中时间减少的最大因素是使用多个核心。\n使用 optdatas 和 optreturn 的测试运行分别实现了约 22.19% 和 15.62% 的速度提升（两者结合则提升了 32.34%）。\n示例使用 # $ ./optimization.py --help usage: optimization.py [-h] [--data DATA] [--fromdate FROMDATE] [--todate TODATE] [--maxcpus MAXCPUS] [--no-runonce] [--exactbars EXACTBARS] [--no-optdatas] [--no-optreturn] [--ma_low MA_LOW] [--ma_high MA_HIGH] [--m1_low M1_LOW] [--m1_high M1_HIGH] [--m2_low M2_LOW] [--m2_high M2_HIGH] [--m3_low M3_LOW] [--m3_high M3_HIGH] Optimization optional arguments: -h, --help show this help message and exit --data DATA, -d DATA data to add to the system --fromdate FROMDATE, -f FROMDATE Starting date in YYYY-MM-DD format --todate TODATE, -t TODATE Starting date in YYYY-MM-DD format --maxcpus MAXCPUS, -m MAXCPUS Number of CPUs to use in the optimization - 0 (default): use all available CPUs - 1 -\u0026gt; n: use as many as specified --no-runonce Run in next mode --exactbars EXACTBARS Use the specified exactbars still compatible with preload 0 No memory savings -1 Moderate memory savings -2 Less moderate memory savings --no-optdatas Do not optimize data preloading in optimization --no-optreturn Do not optimize the returned values to save time --ma_low MA_LOW SMA range low to optimize --ma_high MA_HIGH SMA range high to optimize --m1_low M1_LOW MACD Fast MA range low to optimize --m1_high M1_HIGH MACD Fast MA range high to optimize --m2_low M2_LOW MACD Slow MA range low to optimize --m2_high M2_HIGH MACD Slow MA range high to optimize --m3_low M3_LOW MACD Signal range low to optimize --m3_high M3_HIGH MACD Signal range high to optimize (C) 2015-2024 Daniel Rodriguez ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/05-cerebro/04-optimization-improvements/","section":"教程","summary":"Backtrader 通过引入两个新的 Cerebro 参数，优化了在多进程环境下管理数据源和结果的方式。\n参数说明：\n参数名 默认值 描述 optdatas True 如果为True并进行优化（系统可以预加载并使用runonce），则数据预加载将仅在主进程中进行一次，以节省时间和资源。 optreturn True 如果为True，优化结果将不是完整的策略对象（包括所有数据、指标、观察者等），而是带有以下属性的对象（与策略中相同）：- params（或p）：策略执行时的参数- analyzers：策略执行的分析器 通常只需查看策略参数组合的最终表现（如收益率）。如需查看运行过程中的详细数据（如每个时间点的指标值），请关闭此选项。\n","title":"参数优化性能改进","type":"docs"},{"content":"在动手前，需要先理解两个关键概念：Line（线）和索引 0（Index 0）。\nLine # 在 Backtrader 中，几乎一切都由 Line 构成。无论是价格数据、指标还是策略内部变量，都以 Line 的形式存在。\n你可以把 Line 理解为一条随时间变化的数据序列，由一系列时间点上的数据构成，例如收盘价随时间变化的轨迹。\n一个典型的行情数据源（DataFeed）包含以下关键数据点：\n开盘价（Open） 最高价（High） 最低价（Low） 收盘价（Close） 成交量（Volume） 未平仓量（OpenInterest） 沿着时间轴看，每个数据点各自形成一条独立的线：所有收盘价构成 Close Line，所有开盘价构成 Open Line。因此一个数据源包含 6 条线，再加上用于标识时间的 DateTime，一共是 7 条线。\n在指标（Indicator）中也是如此。简单移动平均线（SMA）根据收盘价计算平均值序列，这个序列同样是一条 Line。布林带、RSI、MACD 等指标都会在内部生成若干条 Line。\n在 Backtrader 中，Line 是一切的基础结构单位：\n数据 → Line 指标 → Line 的运算结果 策略 → 对多条 Line 的逻辑组合与判断 索引0 # 理解 Line 之后，另一个核心概念是索引（Index）。在 Python 中，[0] 通常表示第一个元素，[-1] 表示最后一个元素。但 Backtrader 中有所不同：\n索引 0（[0]）表示当前时刻的值 索引 -1（[-1]）表示上一个时间点的值 索引 -2（[-2]）表示再上一个时间点的值，以此类推 例如，假设在策略初始化阶段创建了一个简单移动平均线（SMA）：\nself.sma = bt.indicators.SimpleMovingAverage(self.data.close, period=15) 当前的 SMA 值是：\ncurrent_value = self.sma[0] 上一个时间点的 SMA 值：\nprevious_value = self.sma[-1] 继续往前推：\ntwo_bars_ago = self.sma[-2] 这样，我们就可以方便地比较过去几个时间点，判断趋势变化、触发信号等。\n# 收盘价刚刚从下方突破均线，则买入 if self.data.close[0] \u0026gt; self.sma[0] and self.data.close[-1] \u0026lt;= self.sma[-1]: self.buy() 这段逻辑就是一个典型的 \u0026ldquo;均线突破买入信号\u0026rdquo;。通过索引操作，我们无需关心当前是哪一天或第几根K线，只需直接比较“现在”和“过去”的数值即可。\nLine 和索引的应用 # 理解了 Line 和索引 0，你就掌握了 Backtrader 的”语言”。编写指标或策略逻辑时，会经常看到类似这样的写法：\nself.rsi = bt.indicators.RSI(self.data.close) RSI 指标生成了一条新的 Line。\nif self.rsi[0] \u0026gt; 70: self.sell() 我们通过 rsi[0] 访问当前 RSI 值，当 RSI 超过 70 时，执行卖出操作。\n无论是移动平均、布林带、RSI，还是自定义指标，最终都是 Line + 索引访问 的形式。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/0401-basic-concept/","section":"教程","summary":"在动手前，需要先理解两个关键概念：Line（线）和索引 0（Index 0）。\nLine # 在 Backtrader 中，几乎一切都由 Line 构成。无论是价格数据、指标还是策略内部变量，都以 Line 的形式存在。\n","title":"策略开发的核心概念","type":"docs"},{"content":"实际交易中常需要结合多个时间框架来制定决策，如在周线评估趋势、在日线执行入场，或基于 5 分钟与 60 分钟数据的对比进行交易。在 Backtrader 中需要将不同时间框架的数据组合在一起。\n本节介绍如何在 Backtrader 中实现多周期策略。\n基本规则 # Backtrader 原生支持多时间框架的数据组合，只需遵循几个简单的规则。\n第一步，最小时间框架的数据必须最先加载。较小时间框架（数据条数最多）应首先加载到 Cerebro 实例中。\n第二步，数据必须按日期时间对齐，以确保平台能正确解析并执行策略。\n第三步，使用 resampledata 将数据重采样到较大时间框架。cerebro.resample 函数可以轻松实现。\n在此基础上，可以在较短和较长时间框架上使用不同的指标。注意，大时间框架的指标产生的信号较少，且 Backtrader 会考虑大时间框架的最小周期以确保数据准确性。\n示例：如何使用多个时间框架 # 下面演示在 Backtrader 中实现多时间周期的步骤。\n加载数据 # 首先，加载较小时间框架的数据。\ndata = btfeeds.BacktraderCSVData(dataname=datapath) 将数据添加到Cerebro # 将较小时间框架数据添加到 Cerebro 实例中。\ncerebro.adddata(data) 重采样数据 # 使用 cerebro.resampledata 将数据重采样到较大的时间框架。\ncerebro.resampledata(data, timeframe=tframes[args.timeframe], compression=args.compression) 运行策略 # 执行策略并生成结果。\ncerebro.run() 示例 # 先演示每日和每周时间框架。假设要在一个策略中同时使用每日和每周数据，通过命令行指定时间框架为每周并进行重采样：\n$ ./multitimeframe-example.py --timeframe weekly --compression 1 此时，程序会加载每日数据，并将其重采样为每周数据。最终输出将包括每周和每日数据的合成图表。\n继续用每日时间框架压缩。如果希望将每日数据压缩为每两天一条，可以使用以下命令：\n$ ./multitimeframe-example.py --timeframe daily --compression 2 此时，Backtrader会将每日数据压缩为每两天一条数据，并生成合成图表。\n还可以加入 SMA 指标来展示不同时间框架的影响。SMA 将在大小时间框架上分别应用，产生不同的信号。\n在较小时间框架（如每日）上，SMA 在第 10 个数据点后首次计算。 在较大时间框架（如每周）上，SMA 的计算会延迟，需要 10 个周期才产生有效信号。 由于 Backtrader 的多时间框架支持，较大时间框架会消耗多个较小时间框架的数据条目来计算指标。\n使用 SMA 时，如果数据点来自较大时间框架，nextstart 方法的调用可能延迟。例如在每周框架下，SMA 需要 10 周的数据，过程中会多次触发 nextstart，因为 Backtrader 会等待所有数据齐全后才执行策略逻辑。\n代码示例 # # 导入必要的库 from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind # 创建SMA策略 class SMAStrategy(bt.Strategy): params = ( (\u0026#39;period\u0026#39;, 10), # SMA的周期 (\u0026#39;onlydaily\u0026#39;, False), # 是否只在每日时间框架上应用 ) def __init__(self): # 为较小时间框架添加SMA self.sma_small_tf = btind.SMA(self.data, period=self.p.period) # 如果选择不只应用于每日时间框架 if not self.p.onlydaily: # 为较大时间框架（如每周）添加SMA self.sma_large_tf = btind.SMA(self.data1, period=self.p.period) # nextstart方法，用于输出调试信息 def nextstart(self): print(\u0026#39;--------------------------------------------------\u0026#39;) print(\u0026#39;nextstart called with len\u0026#39;, len(self)) print(\u0026#39;--------------------------------------------------\u0026#39;) super(SMAStrategy, self).nextstart() # 运行策略 def runstrat(): args = parse_args() # 创建Cerebro实例 cerebro = bt.Cerebro(stdstats=False) # 根据用户选择的策略参数加载相应策略 if not args.indicators: cerebro.addstrategy(bt.Strategy) else: cerebro.addstrategy(SMAStrategy, period=args.period, onlydaily=args.onlydaily) # 加载数据文件 datapath = args.dataname or \u0026#39;../../datas/2006-day-001.txt\u0026#39; data = btfeeds.BacktraderCSVData(dataname=datapath) cerebro.adddata(data) # 添加较小时间框架的数据 tframes = dict(daily=bt.TimeFrame.Days, weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) # 根据需要重采样数据到较大时间框架 if args.noresample: datapath = args.dataname2 or \u0026#39;../../datas/2006-week-001.txt\u0026#39; data2 = btfeeds.BacktraderCSVData(dataname=datapath) cerebro.adddata(data2) else: cerebro.resampledata(data, timeframe=tframes[args.timeframe], compression=args.compression) # 执行策略并生成结果 cerebro.run() # 绘制结果 cerebro.plot(style=\u0026#39;bar\u0026#39;) # 解析命令行参数 def parse_args(): parser = argparse.ArgumentParser(description=\u0026#39;Multitimeframe test\u0026#39;) parser.add_argument(\u0026#39;--dataname\u0026#39;, default=\u0026#39;\u0026#39;, required=False, help=\u0026#39;数据文件路径\u0026#39;) parser.add_argument(\u0026#39;--dataname2\u0026#39;, default=\u0026#39;\u0026#39;, required=False, help=\u0026#39;第二个数据文件路径\u0026#39;) parser.add_argument(\u0026#39;--noresample\u0026#39;, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;不进行数据重采样\u0026#39;) parser.add_argument(\u0026#39;--timeframe\u0026#39;, default=\u0026#39;weekly\u0026#39;, choices=[\u0026#39;daily\u0026#39;, \u0026#39;weekly\u0026#39;, \u0026#39;monthly\u0026#39;], help=\u0026#39;重采样时间框架\u0026#39;) parser.add_argument(\u0026#39;--compression\u0026#39;, default=1, type=int, help=\u0026#39;压缩数据条数\u0026#39;) parser.add_argument(\u0026#39;--indicators\u0026#39;, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;是否使用带指标的策略\u0026#39;) parser.add_argument(\u0026#39;--onlydaily\u0026#39;, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;仅在每日时间框架上应用指标\u0026#39;) parser.add_argument(\u0026#39;--period\u0026#39;, default=10, type=int, help=\u0026#39;指标周期\u0026#39;) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() 结论 # Backtrader 的多时间框架支持让你轻松结合不同时间框架的数据，实现更灵活的交易策略。按照上述规则，即可在多个时间框架上应用不同指标，并调整策略执行逻辑。\n此外，Backtrader 还提供 nextstart 方法精确控制每个周期的数据，方便跟踪和调试。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/05-datafeed-multiple-timeframes/","section":"教程","summary":"实际交易中常需要结合多个时间框架来制定决策，如在周线评估趋势、在日线执行入场，或基于 5 分钟与 60 分钟数据的对比进行交易。在 Backtrader 中需要将不同时间框架的数据组合在一起。\n","title":"多时间框架数据源","type":"docs"},{"content":" 索引：0 和 -1 # 在 Backtrader 中，Line 代表一组按时间顺序排列的点。这些点在策略执行期间动态生成，可通过索引访问。\n使用索引访问 Line # 访问当前值： 使用 0 索引访问当前的线值，如 self.data.close[0] 获取当前收盘价。\nclass MyStrategy(bt.Strategy): def next(self): print(self.data.close[0]) # 当前的收盘价 访问前值： 使用负数索引访问之前的值，如 self.data.close[-1] 获取上一条数据的收盘价。\nclass MyStrategy(bt.Strategy): def next(self): if self.data.close[0] \u0026gt; self.data.close[-1]: print(\u0026#34;今天的收盘价高于昨日的收盘价\u0026#34;) 索引的意义：\n0 索引指向当前时刻的值，-1 指向上一个时刻的值，以此类推。 负数索引指向历史数据点，对时间序列分析和策略数据回溯非常有用。 简单示例：\nclass MyStrategy(bt.Strategy): def next(self): # 比较今天的收盘价和昨天的收盘价 if self.data.close[0] \u0026gt; self.data.close[-1]: print(\u0026#34;今天的收盘价更高\u0026#34;) else: print(\u0026#34;今天的收盘价更低\u0026#34;) 在这个示例中，self.data.close[0] 是今天的收盘价，self.data.close[-1] 是昨天的收盘价。\n切片 # Backtrader 不支持对 Line 对象进行切片操作，这是为了保持设计一致性。切片适用于普通 Python 数组，但 Line 对象是动态增长的，因此切片存在限制。\n为什么不支持切片 # 首先是为了保持 Line 设计的一致性。\nLine 对象的数据通过索引（如 0 和 -1）动态访问，基于时间序列处理数据。切片在此不适用，因为数据是按时间顺序排列的。\n而常规可索引对象的切片是什么样的？\n对于普通的 Python 对象，切片操作如下：\nmy_list = [1, 2, 3, 4, 5] sliced_list = my_list[1:3] # 返回 [2, 3] Backtrader 中的 Line 对象更像一个流式数据源，不能直接进行切片。\n如何获取线的某些点 # 虽然 Line 不支持切片，但你仍然可以使用 get() 方法来获取线的一部分数据。例如：\n# 获取最后 10 个数据点 myslice = self.my_sma.get(size=10) # 获取最近的 10 个值 这样可以获取指定数量的最近历史数据。\n延迟索引 # 在 Backtrader 中，延迟索引允许在 __init__ 阶段访问历史数据，无需手动使用负索引。通常 [] 操作符在 next 阶段提取数据值，而延迟索引可在初始化阶段引用历史数据，生成可在 next 中直接使用的 Lines 对象。\n例如，比较前一日的收盘价与当前简单移动平均线（SMA），无需在每次迭代中手动比较，可通过预先生成的 Lines 对象来实现：\n初始化时使用延迟索引 # 在 __init__ 方法中，你可以通过延迟索引定义需要的历史数据。例如，比较前一天的收盘价和当前的移动平均值：\nclass MyStrategy(bt.Strategy): params = dict(period=20) def __init__(self): self.movav = btind.SimpleMovingAverage(self.data, period=self.p.period) self.cmpval = self.data.close(-1) \u0026gt; self.movav # 比较前一日收盘价与当前20日均线 这里，self.data.close(-1) 通过延迟索引获取前一天的收盘价，self.movav 是当前的20日简单移动平均线。self.cmpval 是一个新的 Lines 对象，保存每次比较的结果。\n在 next 方法中使用延迟数据 # 在 next 方法中，你可以直接使用 Lines 对象的值进行逻辑判断。self.cmpval[0] 会返回当前条件是否成立：\nclass MyStrategy(bt.Strategy): def next(self): if self.cmpval[0]: # 如果前一日收盘价高于当前移动平均线 print(\u0026#34;前一日收盘价高于当前简单移动平均线\u0026#34;) 延迟索引的工作原理 # self.data.close(-1) 使用延迟索引从历史数据中提取前一天的收盘价。它返回一个 Line 对象，但带有延迟偏移，即相对于当前时刻，它表示前一个时间点的 Line。\n语句 self.data.close(-1) \u0026gt; self.movav 生成一个新的 Line 对象，该对象在 next 中返回 1（真）或 0（假），可在 next 中直接使用比较结果。\n通过这种方式，Backtrader 提供了简洁灵活的方式来引用和比较历史数据，无需手动管理索引，简化了策略编写和逻辑实现。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/04-index/","section":"教程","summary":"索引：0 和 -1 # 在 Backtrader 中，Line 代表一组按时间顺序排列的点。这些点在策略执行期间动态生成，可通过索引访问。\n","title":"索引与切片操作详解","type":"docs"},{"content":"Fyne标准控件提供最小的功能和自定义选项以支持大多数用例。在某些时候可能需要更高级的功能。与其让开发者构建自己的控件，不如扩展现有的控件。\n例如，我们将扩展图标控件以支持被点击。为此，我们声明一个新的结构体，嵌入了widget.Icon类型。我们创建一个构造函数，调用重要的ExtendBaseWidget函数。\nimport ( \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) type tappableIcon struct { widget.Icon } func newTappableIcon(res fyne.Resource) *tappableIcon { icon := \u0026amp;tappableIcon{} icon.ExtendBaseWidget(icon) icon.SetResource(res) return icon } 注意： 像widget.NewIcon这样的控件构造函数可能不适用于扩展，因为它已经调用了ExtendBaseWidget。\n然后，我们添加新函数以实现fyne.Tappable接口，有了这些函数，每次用户点击我们的新图标时都会调用新的Tapped函数。接口需要有两个函数，Tapped(*PointEvent)和TappedSecondary(*PointEvent)，所以我们将添加这两个。\nimport \u0026#34;log\u0026#34; func (t *tappableIcon) Tapped(_ *fyne.PointEvent) { log.Println(\u0026#34;I have been tapped\u0026#34;) } func (t *tappableIcon) TappedSecondary(_ *fyne.PointEvent) { } 我们可以使用如下简单的应用程序测试这个新控件。\nimport ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/theme\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;Tappable\u0026#34;) w.SetContent(newTappableIcon(theme.FyneLogo())) w.ShowAndRun() } ","date":"2025-05-02","externalUrl":null,"permalink":"/docs/gofyne/08-extend/05-extending-widgets/","section":"教程","summary":"Fyne标准控件提供最小的功能和自定义选项以支持大多数用例。在某些时候可能需要更高级的功能。与其让开发者构建自己的控件，不如扩展现有的控件。\n例如，我们将扩展图标控件以支持被点击。为此，我们声明一个新的结构体，嵌入了widget.Icon类型。我们创建一个构造函数，调用重要的ExtendBaseWidget函数。\n","title":"扩展控件 Widget","type":"docs"},{"content":"为了展示如何连接更复杂的类型，我们将看看List控件以及数据绑定如何使其更易用。首先，我们创建一个StringList数据绑定，这是一个String数据类型的列表。一旦我们有了列表类型的数据，我们就可以将这个数据连接到标准的List控件。为此，我们使用widget.NewListWithData构造函数，这和其他控件类似。\n将这段代码与列表教程进行比较，你会看到两个主要变化，第一个是我们将数据类型作为第一个参数传递，而不是长度回调函数。第二个变化是最后一个参数，我们的UpdateItem回调。修订版采用binding.DataItem值而不是widget.ListIndexID。使用这种回调结构时，我们应该Bind到模板标签控件而不是调用SetText。这意味着如果数据源中的任何字符串发生变化，表格的每个受影响行都将刷新。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/data/binding\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;List Data\u0026#34;) data := binding.BindStringList( \u0026amp;[]string{\u0026#34;Item 1\u0026#34;, \u0026#34;Item 2\u0026#34;, \u0026#34;Item 3\u0026#34;}, ) list := widget.NewListWithData(data, func() fyne.CanvasObject { return widget.NewLabel(\u0026#34;template\u0026#34;) }, func(i binding.DataItem, o fyne.CanvasObject) { o.(*widget.Label).Bind(i.(binding.String)) }) add := widget.NewButton(\u0026#34;Append\u0026#34;, func() { val := fmt.Sprintf(\u0026#34;Item %d\u0026#34;, data.Length()+1) data.Append(val) }) myWindow.SetContent(container.NewBorder(nil, add, nil, nil, list)) myWindow.ShowAndRun() } 在我们的演示代码中，有一个“Append”按钮，当点击时，它会向数据源追加一个新值。这样做将自动触发数据变化处理程序并扩展List控件以显示新数据。\n","date":"2025-04-14","externalUrl":null,"permalink":"/docs/gofyne/07-binding/04-list/","section":"教程","summary":"为了展示如何连接更复杂的类型，我们将看看List控件以及数据绑定如何使其更易用。首先，我们创建一个StringList数据绑定，这是一个String数据类型的列表。一旦我们有了列表类型的数据，我们就可以将这个数据连接到标准的List控件。为此，我们使用widget.NewListWithData构造函数，这和其他控件类似。\n","title":"列表类型","type":"docs"},{"content":"表单控件用于排列许多输入字段、标签以及可选的取消和提交按钮。在其最简单的形式中，它将标签对齐到每个输入控件的左侧。通过设置OnCancel或OnSubmit，表单将添加一个按钮栏，当适当时调用指定的处理程序。\n可以通过传递widget.FormItem列表使用widget.NewForm(...)创建控件，或者使用示例中所示的\u0026amp;widget.Form{}语法。还有一个有用的Form.Append(label, widget)，可用于另一种语法。\n在这个例子中，我们创建了两个输入框，其中一个是“多行”的（类似HTML TextArea）来保存值。有一个OnSubmit处理程序，在关闭窗口（因此是应用程序）之前打印信息。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;表单控件\u0026#34;) entry := widget.NewEntry() textArea := widget.NewMultiLineEntry() form := \u0026amp;widget.Form{ Items: []*widget.FormItem{ // 我们可以在构造函数中指定项 {Text: \u0026#34;输入框\u0026#34;, Widget: entry}}, OnSubmit: func() { // 可选，处理表单提交 log.Println(\u0026#34;表单提交：\u0026#34;, entry.Text) log.Println(\u0026#34;多行：\u0026#34;, textArea.Text) myWindow.Close() }, } // 我们也可以追加项目 form.Append(\u0026#34;文本\u0026#34;, textArea) myWindow.SetContent(form) myWindow.ShowAndRun() } ","date":"2025-03-09","externalUrl":null,"permalink":"/docs/gofyne/05-widget/05-form/","section":"教程","summary":"表单控件用于排列许多输入字段、标签以及可选的取消和提交按钮。在其最简单的形式中，它将标签对齐到每个输入控件的左侧。通过设置OnCancel或OnSubmit，表单将添加一个按钮栏，当适当时调用指定的处理程序。\n","title":"表单 Form","type":"docs"},{"content":"","date":"2025-02-22","externalUrl":null,"permalink":"/docs/gofyne/05-widget/","section":"教程","summary":"","title":"控件","type":"docs"},{"content":"layout.FormLayout 类似于两列的 网格布局，但针对应用中的表单布局进行了调整。每个项目的高度将是每行中两个最小高度中的较大者。第一列中所有项目的最大最小宽度将是左侧项目的宽度，而每行中的第二个项目将扩展以填满空间。\n这种布局更典型地用于 widget.Form（用于验证、提交和取消按钮等），但也可以直接使用 layout.NewFormLayout() 作为 container.New(...) 的第一个参数。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/layout\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;表单布局\u0026#34;) label1 := widget.NewLabel(\u0026#34;标签 1\u0026#34;) value1 := widget.NewLabel(\u0026#34;值\u0026#34;) label2 := widget.NewLabel(\u0026#34;标签 2\u0026#34;) value2 := widget.NewLabel(\u0026#34;某些内容\u0026#34;) grid := container.New(layout.NewFormLayout(), label1, value1, label2, value2) myWindow.SetContent(grid) myWindow.ShowAndRun() } ","date":"2025-02-10","externalUrl":null,"permalink":"/docs/gofyne/04-container/05-form/","section":"教程","summary":"layout.FormLayout 类似于两列的 网格布局，但针对应用中的表单布局进行了调整。每个项目的高度将是每行中两个最小高度中的较大者。第一列中所有项目的最大最小宽度将是左侧项目的宽度，而每行中的第二个项目将扩展以填满空间。\n","title":"表单布局 Form","type":"docs"},{"content":"canvas.Image 在 Fyne 中代表一个可缩放的图像资源。它可以从资源（如示例所示）、图像文件、包含图像的 URI 位置、io.Reader 或内存中的 Go image.Image 加载。\n默认的图像填充模式是 canvas.ImageFillStretch，它会导致图像填充指定的空间（通过 Resize() 或布局）。或者，你可以使用 canvas.ImageFillContain 以确保保持纵横比并且图像在边界内。此外，你可以使用 canvas.ImageFillOriginal（如此示例中所用），以确保它也具有等于原始图像大小的最小尺寸。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/theme\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;图像\u0026#34;) image := canvas.NewImageFromResource(theme.FyneLogo()) // image := canvas.NewImageFromURI(uri) // image := canvas.NewImageFromImage(src) // image := canvas.NewImageFromReader(reader, name) // image := canvas.NewImageFromFile(fileName) image.FillMode = canvas.ImageFillOriginal w.SetContent(image) w.ShowAndRun() } 图像可以是基于位图的（如 PNG 和 JPEG）或基于矢量的（如 SVG）。我们建议尽可能使用可缩放图像，因为它们在大小变化时继续渲染良好。在使用原始图像大小时要小心，因为它们可能不会完全按预期与不同的用户界面比例一致。由于 Fyne 允许整个用户界面缩放，一个 25px 的图像文件可能与一个 25 高度的 fyne 对象不同。\n","date":"2025-01-14","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/05-image/","section":"教程","summary":"canvas.Image 在 Fyne 中代表一个可缩放的图像资源。它可以从资源（如示例所示）、图像文件、包含图像的 URI 位置、io.Reader 或内存中的 Go image.Image 加载。\n默认的图像填充模式是 canvas.ImageFillStretch，它会导致图像填充指定的空间（通过 Resize() 或布局）。或者，你可以使用 canvas.ImageFillContain 以确保保持纵横比并且图像在边界内。此外，你可以使用 canvas.ImageFillOriginal（如此示例中所用），以确保它也具有等于原始图像大小的最小尺寸。\n","title":"图片 Image","type":"docs"},{"content":" 颜色 # 允许用户从标准集合中选择一个颜色（或在高级模式中选择任何颜色）。\n确认 # 请求确认一个操作。\n文件打开 # 提示用户选择一个文件以在应用内使用。实际显示的对话框将取决于当前操作系统。\n表单 # 在一个对话框中获取各种输入元素，并进行验证。\n信息 # 一种向应用用户展示一些信息的简单方法。\n自定义 # 在对话框容器内展示任何内容。\n","date":"2024-12-09","externalUrl":null,"permalink":"/docs/gofyne/02-explore/05-dialogs/","section":"教程","summary":"颜色 # 允许用户从标准集合中选择一个颜色（或在高级模式中选择任何颜色）。\n","title":"Dialog 对话框","type":"docs"},{"content":"在完成了Hello World教程或其他示例之后，你将创建一个基本的用户界面。在这个页面中，我们将看到如何从代码中更新GUI的内容。\n第一步是将你想要更新的控件赋值给一个变量。在Hello World教程中，我们直接将widget.NewLabel传递给SetContent()，为了更新它，我们将其更改为两行不同的代码，例如：\nclock := widget.NewLabel(\u0026#34;\u0026#34;) w.SetContent(clock) 一旦内容被赋值给一个变量，我们就可以调用像SetText(\u0026quot;new text\u0026quot;)这样的函数。在我们的示例中，我们将使用Time.Format的帮助，将标签的内容设置为当前时间。\nformatted := time.Now().Format(\u0026#34;Time: 03:04:05\u0026#34;) clock.SetText(formatted) 这就是我们需要做的，以改变一个可见项的内容（见下面的完整代码）。然而，我们可以进一步定期更新内容。\n在后台运行 # 大多数应用程序都需要在后台运行进程，例如下载数据或响应事件。为了模拟这一点，我们将扩展上述代码，使其每秒运行一次。\n像大多数Go代码一样，我们可以创建一个goroutine（使用go关键字）并在那里运行我们的代码。如果我们将文本更新代码移动到一个新函数中，它可以在初始显示以及定期更新时被调用。通过组合goroutine和time.Tick在一个for循环中，我们可以每秒更新标签。\ngo func() { for range time.Tick(time.Second) { updateTime(clock) } }() 将这段代码放在ShowAndRun或Run调用之前是很重要的，因为它们在应用程序关闭之前不会返回。将所有这些结合在一起，代码将运行并每秒更新用户界面，创建一个基本的时钟控件。完整的代码如下：\npackage main import ( \u0026#34;time\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func updateTime(clock *widget.Label) { formatted := time.Now().Format(\u0026#34;Time: 03:04:05\u0026#34;) clock.SetText(formatted) } func main() { a := app.New() w := a.NewWindow(\u0026#34;Clock\u0026#34;) clock := widget.NewLabel(\u0026#34;\u0026#34;) updateTime(clock) w.SetContent(clock) go func() { for range time.Tick(time.Second) { updateTime(clock) } }() w.ShowAndRun() } 这段代码演示了如何在Fyne应用程序中创建动态更新的内容，这是构建交云动用户界面的基础。\n","date":"2024-10-28","externalUrl":null,"permalink":"/docs/gofyne/01-started/05-updating/","section":"教程","summary":"在完成了Hello World教程或其他示例之后，你将创建一个基本的用户界面。在这个页面中，我们将看到如何从代码中更新GUI的内容。\n第一步是将你想要更新的控件赋值给一个变量。在Hello World教程中，我们直接将widget.NewLabel传递给SetContent()，为了更新它，我们将其更改为两行不同的代码，例如：\n","title":"更新 GUI 内容","type":"docs"},{"content":"不知道你是否想过自定义 Shell 提示符主题能带来的不仅是终端美观度的提升，还能通过视觉优化增强工作效率呢？\n在众多 shell 提示符主题中，Powerlevel10k 因为支持高度可定制和丰富的功能选项，非常值得推荐。本文基于这个主题介绍 zsh 主题插件 Powerlevel10k，包括它的安装和配置自定义。\n安装 Powerlevel10k # 首先安装 Powerlevel10k 本体：\ngit clone --depth=1 https://github.com/romkatv/powerlevel10k.git \\ ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k 然后在 ~/.zshrc 中将主题设置为 Powerlevel10k：\nZSH_THEME=\u0026#34;powerlevel10k/powerlevel10k\u0026#34; 执行 source ~/.zshrc，如果是第一次安装，p10k 配置向导会自动启动。\n配置向导 # Powerlevel10k 提供了一个交互式配置向导，你只需要回答几个问题即可完成配置：\np10k configure 向导会问你的偏好：\n主题风格 — 经典（雷击图标）还是极简（纯文字） 文字布局 — 单行还是双行 左侧信息 — 时间、路径、git 分支等 右侧信息 — 时间、命令执行时长、shell 层级等 连接线 — 圆角、直角、斜线还是无连接线 字体 — 是否安装推荐字体（Nerd Font 或 Meslo Nerd Font） 每个问题都有实时预览，选完即生效。\n信息区域 # 一个典型配置好的 Powerlevel10k 看起来长这样：\n❯ ~/Projects/myapp on main ⇡1 ⇢2 ❯ 各段含义（从左到右）：\n段 含义 ❯ 提示符符号 ~/Projects/myapp 当前路径（缩短显示） on main 当前 git 分支 ⇡1 git 有 1 个提交未推送 ⇢2 git 有 2 个文件未暂存 右侧还显示上一个命令的执行时长和当前时间——命令跑太久了一目了然。\n自定义配置 # 如果你不想走向导，也可以手动编辑 ~/.p10k.zsh。这个文件每段配置都带了详细的注释说明。\n常用的自定义：\n# 修改左侧元素 POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(dir vcs) # 修改右侧元素 POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(status command_execution_time time) # 显示完整路径（默认缩短家目录为 ~） POWERLEVEL9K_DIR_PATH_ABSOLUTE=true # 命令执行时间超过 3 秒才显示 POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 # 修改图标 POWERLEVEL9K_VCS_GIT_ICON=\u0026#39;\u0026#39; 字体要求 # Powerlevel10k 推荐使用 Nerd Font 字体来显示图标。如果安装时没有安装推荐字体，可以手动安装：\n# Meslo Nerd Font brew install --cask font-meslo-lg-nerd-font 然后在 iTerm2 的 Preferences \u0026gt; Profiles \u0026gt; Text \u0026gt; Font 中选择 MesloLGS Nerd Font。\n小结 # Powerlevel10k 是目前 zsh 主题的天花板，速度快、配置灵活、信息丰富。安装加配置向导走一遍，你的终端提示符就从朴素的黑白文字，变成了信息密度极高的效率工具。\n","date":"2024-03-17","externalUrl":null,"permalink":"/docs/mytermenv/ohmyzsh/powerlevel10k/","section":"教程","summary":"不知道你是否想过自定义 Shell 提示符主题能带来的不仅是终端美观度的提升，还能通过视觉优化增强工作效率呢？\n在众多 shell 提示符主题中，Powerlevel10k 因为支持高度可定制和丰富的功能选项，非常值得推荐。本文基于这个主题介绍 zsh 主题插件 Powerlevel10k，包括它的安装和配置自定义。\n","title":"自定义提示主题","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/05-cerebro/","section":"教程","summary":"","title":"Cerebro","type":"docs"},{"content":" 欢迎消息配置 — motd、自定义欢迎脚本、待办提醒 系统信息工具 — neofetch、fastfetch、pfetch 等系统信息展示工具 ","externalUrl":null,"permalink":"/docs/mytermenv/startup/","section":"教程","summary":" 欢迎消息配置 — motd、自定义欢迎脚本、待办提醒 系统信息工具 — neofetch、fastfetch、pfetch 等系统信息展示工具 ","title":"欢迎消息","type":"docs"},{"content":"在之前的文章中，介绍了 MFI（Money Flow Indicator，资金流动指标）的实现。\n虽然该实现按传统方式开发，但仍有改进空间，可以做得更通用。\n先关注实现的前几行——计算典型价格的部分。\nCanonical MFI - 典型价格和原始资金流 # class MFI_Canonical(bt.Indicator): lines = (\u0026#39;mfi\u0026#39;,) params = dict(period=14) def __init__(self): tprice = (self.data.close + self.data.low + self.data.high) / 3.0 mfraw = tprice * self.data.volume ... 典型的实例化方式：\nMFI 典型实例化 # class MyMFIStrategy(bt.Strategy): def __init__(self): mfi = bt.MFI_Canonical(self.data) 这里的问题很明显：需要为指标提供包含收盘价、最低价、最高价和成交量的输入（即 backtrader 生态中的 lines）。\n当然，可能有人希望使用来自不同数据源的组件来创建 MFI（例如来自数据源或其他指标的线）。比如想给收盘价更多权重，而不必开发特定指标。考虑到行业标准的 OHLCV 数据字段顺序，支持多输入并给收盘价加权的实例化可以是：\nMFI 多输入实例化 # class MyMFIStrategy2(bt.Strategy): def __init__(self): wclose = self.data.close * 5.0 mfi = bt.MFI_Canonical(self.data.high, self.data.low, wclose, self.data.volume) 或者因为用户之前使用过 ta-lib 并喜欢多个输入的方式。\n支持多个输入 # backtrader 尽量保持 Python 风格。系统中的 self.datas 数组（自动提供给策略的所有数据源）可以查询长度。可以利用这一点区分调用者需求，正确计算 tprice 和 mfraw。\nMFI - 使用 len 的多个输入 # class MFI_MultipleInputs(bt.Indicator): lines = (\u0026#39;mfi\u0026#39;,) params = dict(period=14) def __init__(self): if len(self.datas) == 1: # 传入一个数据源，必须包含各个组件 tprice = (self.data.close + self.data.low + self.data.high) / 3.0 mfraw = tprice * self.data.volume else: # 如果有多个数据源，按照 OHLCV 顺序传入每个组件 tprice = (self.data0 + self.data1 + self.data2) / 3.0 mfraw = tprice * self.data3 # 与之前的实现无变化 flowpos = bt.ind.SumN(mfraw * (tprice \u0026gt; tprice(-1)), period=self.p.period) flowneg = bt.ind.SumN(mfraw * (tprice \u0026lt; tprice(-1)), period=self.p.period) mfiratio = bt.ind.DivByZero(flowpos, flowneg, zero=100.0) self.l.mfi = 100.0 - 100.0 / (1.0 + mfiratio) 注意 # 各组件通过 self.dataX（如 self.data0、self.data1）引用。\n这与 self.datas[x]（如 self.datas[0]）是等价的。\n接下来，通过图形化展示该指标与传统实现的结果是否一致，特别是多输入与数据源原始组件对应时：\nMFI - 结果检查 # class MyMFIStrategy2(bt.Strategy): def __init__(self): MFI_Canonical(self.data) MFI_MultipleInputs(self.data, plotname=\u0026#39;MFI 单输入\u0026#39;) MFI_MultipleInputs(self.data.high, self.data.low, self.data.close, self.data.volume, plotname=\u0026#39;MFI 多输入\u0026#39;) MFI 结果检查 # 通过图示可见，三者的结果应相同，无需逐一检查每个值。\n最后，看看加大收盘价权重的效果：\nMFI - 5 倍收盘价 # class MyMFIStrategy2(bt.Strategy): def __init__(self): MFI_MultipleInputs(self.data) MFI_MultipleInputs(self.data.high, self.data.low, self.data.close * 5.0, self.data.volume, plotname=\u0026#39;MFI 收盘价 * 5.0\u0026#39;) MFI 收盘价 * 5.0 # 是否合理留给读者自行判断，但可以清楚看到，增加收盘价权重后图形模式发生了变化。\n结论 # 通过简单地使用 Pythonic 风格的 len，我们可以将一个使用固定组件名称的数据源的指标转换为一个接受多个通用输入的指标。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/19-articles/06-mfi-generic/","section":"教程","summary":"在之前的文章中，介绍了 MFI（Money Flow Indicator，资金流动指标）的实现。\n虽然该实现按传统方式开发，但仍有改进空间，可以做得更通用。\n先关注实现的前几行——计算典型价格的部分。\n","title":"MFI 资金流指标的通用实现","type":"docs"},{"content":"交易的定义：当某个工具的头寸从 0 变为 X（正数为多头，负数为空头）时，交易开启；当头寸从 X 变为 0 时，交易关闭。\n以下两种情况：\n从正变负 从负变正 实际上被视为：关闭一个交易（头寸从 X 到 0），同时开启一个新交易（头寸从 0 到 Y）。\n交易仅用于信息展示，用户无法调用其方法。\n参考 # class backtrader.trade.Trade(data=None, tradeid=0, historyon=False, size=0, price=0.0, value=0.0, commission=0.0) 追踪交易的生命周期：数量、价格、佣金和价值。交易从 0 开始，可以增加和减少，回到 0 表示关闭。交易可以是多头（正数）或空头（负数）。交易不支持反转。\n成员属性 描述 ref 唯一的交易标识符 status Created, Open, Closed之一 tradeid 在创建订单时传递给订单的分组交易ID，订单的默认值为0 size 当前交易的数量 price 当前交易的价格 value 当前交易的价值 commission 当前累计的佣金 pnl 当前交易的盈亏（毛利） pnlcomm 当前交易的净盈亏（扣除佣金后的净利润） isclosed 记录最后一次更新是否关闭了交易（将交易数量设为零） isopen 记录任何更新是否开启了交易 justopened 如果交易刚刚开启 baropen 交易开启的bar dtopen 交易开启的浮点编码日期时间，使用 open_datetime 方法获取 Python datetime.datetime 或使用平台提供的 num2date 方法 barclose 交易关闭的bar dtclose 交易关闭的浮点编码日期时间，使用 close_datetime 方法获取 Python datetime.datetime 或使用平台提供的 num2date 方法 barlen 交易开启的bar数量 historyon 是否记录历史 history 包含每次“更新”事件更新后的状态和使用的参数的列表，历史记录的第一个条目是开启事件，最后一个条目是关闭事件 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/10-broker/06-trade/","section":"教程","summary":"交易的定义：当某个工具的头寸从 0 变为 X（正数为多头，负数为空头）时，交易开启；当头寸从 X 变为 0 时，交易关闭。\n以下两种情况：\n从正变负 从负变正 实际上被视为：关闭一个交易（头寸从 X 到 0），同时开启一个新交易（头寸从 0 到 Y）。\n","title":"Trade 交易记录详解","type":"docs"},{"content":" 本节我们将学习如何开发策略。\n第一个策略不涉及交易，只用来打印每一天（bar）的收盘价。\n策略类（Strategy）继承自 bt.Strategy。\nclass TestStrategy(bt.Strategy): def __init__(self): pass def next(self): pass 它最重要的两个方法是 __init__（策略初始化）和 next（每个 bar 执行一次）。\nclass TestStrategy(bt.Strategy): def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) self.datas[0].close 访问的是通过 cerebro.adddata 添加的第一个数据源（DataFeed）。\n数据列表 self.datas 是一个标准 Python 列表，按插入顺序存储。第一个数据 self.datas[0] 是默认交易数据，作为系统时钟同步所有策略元素。\nself.datas 中的元素是 DataSeries 类型，可通过别名访问 OHLC 数据。\nopen = data.open low = data.low high = data.high close = data.close 为了便于使用，我们将其赋值到 self.dataclose，简化打印逻辑。\nself.dataclose = self.datas[0].close 接着在 next 方法中打印 self.dataclose[0]，即最新收盘价。策略的 next 方法会在每个新 bar 上调用，使用系统时钟（self.datas[0]）作为参考。\ndef next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) 有了策略类 TestStrategy，还要通过 cerebro.addstrategy 将其添加到交易系统中。\ncerebro.addstrategy(TestStrategy) 完整示例 # import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) import backtrader as bt class TestStrategy(bt.Strategy): def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() cerebro.addstrategy(TestStrategy) modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, \u0026#39;./orcl-1995-2014.txt\u0026#39;) data = bt.feeds.YahooFinanceCSVData( dataname=datapath, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), reverse=False) cerebro.adddata(data) cerebro.broker.setcash(100000.0) print(\u0026#39;Starting Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) cerebro.run() print(\u0026#39;Final Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) 输出：\nStarting Portfolio Value: 100000.00 2000-01-03, Close, 27.85 2000-01-04, Close, 25.39 2000-01-05, Close, 24.05 ... ... ... 2000-12-26, Close, 29.17 2000-12-27, Close, 28.94 2000-12-28, Close, 29.29 2000-12-29, Close, 27.41 Final Portfolio Value: 100000.00 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/04-first-strategy/","section":"教程","summary":" 本节我们将学习如何开发策略。\n第一个策略不涉及交易，只用来打印每一天（bar）的收盘价。\n策略类（Strategy）继承自 bt.Strategy。\n","title":"编写第一个回测策略","type":"docs"},{"content":"在 Backtrader 中，运算符不仅用于常规数学运算，还能构建复杂的策略逻辑。自定义运算符让策略的数学和逻辑运算更自然简洁。\n如何使用运算符 # backtrader 支持用运算符创建新对象，如在 __init__ 中通过运算符计算多个指标，得到一个新的操作对象。\nclass MyStrategy(bt.Strategy): def __init__(self): sma = btind.SimpleMovingAverage(self.data, period=20) # 使用运算符创建新的逻辑对象 close_over_sma = self.data.close \u0026gt; sma sma_dist_to_high = self.data.high - sma sma_dist_small = sma_dist_to_high \u0026lt; 3.5 # 创建卖出信号 self.sell_sig = bt.And(close_over_sma, sma_dist_small) 在 Line 对象上使用常规运算符，如加减乘除、大小比较等，简化了策略代码，增强了可读性和可维护性。\n注：backtrader 的指标计算是自有体系，不是基于 numpy 和 pandas，因此要单独实现这些运算符。\n一些未覆盖的运算符/函数 # Python 中的某些运算符未被覆盖，backtrader 提供了专门的函数来模拟逻辑运算，如 bt.And 和 bt.Or 实现逻辑\u0026quot;与\u0026quot;和\u0026quot;或\u0026quot;。\n下面列出这些单独实现的运算符。\n逻辑运算符 # Python 中的 and 和 or 运算符无法在 Backtrader 中覆盖，backtrader 提供了 bt.And 和 bt.Or 来模拟这两个逻辑操作。\nself.buy_sig = bt.And(self.data.close \u0026gt; self.sma, self.data.high \u0026lt; 50.0) self.sell_sig = bt.Or(self.data.close \u0026lt; self.sma, self.data.low \u0026gt; 30.0) 数学函数 # Backtrader 也提供了替代 Python 标准库函数的方式，如 max 和 min 对应 bt.Max 和 bt.Min。这些函数可用于处理 Line 对象：\nhighest = bt.Max(self.data.high, period=20) # 获取过去20个周期的最高价 lowest = bt.Min(self.data.low, period=20) # 获取过去20个周期的最低价 使用 bt.If 模拟条件分支 # 按条件选择值时，Backtrader 提供了 bt.If 来模拟条件分支。bt.If 类似于 Python 的三元运算符 x if condition else y，或者 numpy 中的 where 函数。\nclass MyStrategy(bt.Strategy): def __init__(self): sma1 = btind.SMA(self.data.close, period=15) # 使用 bt.If 来根据条件选择价格 high_or_low = bt.If(sma1 \u0026gt; self.data.close, self.data.low, self.data.high) sma2 = btind.SMA(high_or_low, period=15) # 使用选中的值计算新的简单移动平均线 其他函数 # 类似地，any 对应 bt.Any，all 对应 bt.All，sum 对应 bt.Sum，cmp 对应 bt.Cmp，reduce 对应 bt.Reduce。\n这些函数都可用于处理可迭代对象，与 Line 对象兼容。\nsum_values = bt.Sum(self.data.close, period=10) # 计算过去10个周期的收盘价总和 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/05-operator/","section":"教程","summary":"在 Backtrader 中，运算符不仅用于常规数学运算，还能构建复杂的策略逻辑。自定义运算符让策略的数学和逻辑运算更自然简洁。\n如何使用运算符 # backtrader 支持用运算符创建新对象，如在 __init__ 中通过运算符计算多个指标，得到一个新的操作对象。\n","title":"内置运算符与数据计算","type":"docs"},{"content":"版本 1.9.32.116 增加了对社区提出的一个有趣用例的支持：\n用期货开仓交易，包括实物交割； 使用指标进行分析； 必要时通过操作现货价格来平仓，从而取消实物交割（收货或交货，希望能获利）； 期货在操作现货价格的当天到期。 这意味着：\n平台接收两个不同资产的数据； 平台需要理解这些资产相关，且现货操作将关闭期货头寸； 实际上期货并未平仓，只是实物交割被补偿了。 基于这一补偿概念，backtrader 允许用户告知平台：一个数据流上的操作会对另一个数据流产生补偿效果。使用方式如下：\nimport backtrader as bt cerebro = bt.Cerebro() data0 = bt.feeds.MyFavouriteDataFeed(dataname=\u0026#39;futurename\u0026#39;) cerebro.adddata(data0) data1 = bt.feeds.MyFavouriteDataFeed(dataname=\u0026#39;spotname\u0026#39;) data1.compensate(data0) # 告诉系统 data1 的操作会影响 data0 cerebro.adddata(data1) ... cerebro.run() 综合示例 # 示例胜过千言万语，下面将所有部分结合起来。\n使用 backtrader 源码中的标准示例数据源作为期货数据。 复用同一数据源，添加一个随机移动价格的过滤器来模拟价差，实现如下： # 更改收盘价的过滤器 def close_changer(data, *args, **kwargs): data.close[0] += 50.0 * random.randint(-1, 1) return False # 数据流长度未变 在同一轴上绘图会混淆默认的 BuyObserver 标记，因此禁用了标准观察者，手动重新添加并使用不同的数据标记。\n头寸随机进入，10 天后退出。\n这并不匹配期货的到期期限，但本例仅演示功能，而非交易日历。\n注意：\n如需在期货到期日模拟现货价格执行，需要激活 “cheat-on-close” 确保订单在期货到期时执行。本例不需要，因为到期是随机选择的。 注意策略中的操作：\n买入操作在 data0 上执行 卖出操作在 data1 上执行 class St(bt.Strategy): def __init__(self): bt.obs.BuySell(self.data0, barplot=True) # 为不同数据添加不同标记 BuySellArrows(self.data1, barplot=True) # 为不同数据添加不同标记 def next(self): if not self.position: if random.randint(0, 1): self.buy(data=self.data0) self.entered = len(self) else: # 在市场中 if (len(self) - self.entered) \u0026gt;= 10: self.sell(data=self.data1) 执行： # $ ./future-spot.py --no-comp 得到如下图形输出。\n可以看到：\n买入操作用向上的绿色三角形标记，图例显示属于 data0 卖出操作用向下箭头标记，图例显示属于 data1 即使在 data0 上开仓、在 data1 上平仓，也能实现交易闭合，避免实物交割。\n如果不用补偿，同样的逻辑会发生什么：\n$ ./future-spot.py --no-comp 这会失败：\n逻辑期望 data0 上的头寸通过 data1 的操作平仓，且仅在无持仓时才在 data0 上开仓 但补偿被禁用，data0 的初始操作（绿色三角形）从未平仓，无法发起其他操作，data1 上的空头头寸开始累积。 示例用法： # $ ./future-spot.py --help usage: future-spot.py [-h] [--no-comp] Compensation example optional arguments: -h, --help show this help message and exit --no-comp 示例代码： # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import random import backtrader as bt # 更改收盘价的过滤器 def close_changer(data, *args, **kwargs): data.close[0] += 50.0 * random.randint(-1, 1) return False # 数据流长度未变 # 重写标准标记 class BuySellArrows(bt.observers.BuySell): plotlines = dict(buy=dict(marker=\u0026#39;$\\u21E7$\u0026#39;, markersize=12.0), sell=dict(marker=\u0026#39;$\\u21E9$\u0026#39;, markersize=12.0)) class St(bt.Strategy): def __init__(self): bt.obs.BuySell(self.data0, barplot=True) # 为不同数据添加不同标记 BuySellArrows(self.data1, barplot=True) # 为不同数据添加不同标记 def next(self): if not self.position: if random.randint(0, 1): self.buy(data=self.data0) self.entered = len(self) else: # 在市场中 if (len(self) - self.entered) \u0026gt;= 10: self.sell(data=self.data1) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() dataname = \u0026#39;../../datas/2006-day-001.txt\u0026#39; # 数据源 data0 = bt.feeds.BacktraderCSVData(dataname=dataname, name=\u0026#39;data0\u0026#39;) cerebro.adddata(data0) data1 = bt.feeds.BacktraderCSVData(dataname=dataname, name=\u0026#39;data1\u0026#39;) data1.addfilter(close_changer) if not args.no_comp: data1.compensate(data0) data1.plotinfo.plotmaster = data0 cerebro.adddata(data1) cerebro.addstrategy(St) # 示例策略 cerebro.addobserver(bt.obs.Broker) # 以下两行在 stdstats=False 时被移除 cerebro.addobserver(bt.obs.Trades) cerebro.broker.set_coc(True) cerebro.run(stdstats=False) # 执行 cerebro.plot(volume=False) # 绘图 def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=(\u0026#39;Compensation example\u0026#39;)) parser.add_argument(\u0026#39;--no-comp\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/09-orders/06-future-spot-compensation/","section":"教程","summary":"版本 1.9.32.116 增加了对社区提出的一个有趣用例的支持：\n用期货开仓交易，包括实物交割； 使用指标进行分析； 必要时通过操作现货价格来平仓，从而取消实物交割（收货或交货，希望能获利）； 期货在操作现货价格的当天到期。 这意味着：\n","title":"期现货补偿机制详解","type":"docs"},{"content":"当只有单一时间框架的数据可用，而分析需要在不同时间框架上进行时，就需要重采样。\u0026ldquo;重采样\u0026rdquo; 实际应称为 \u0026ldquo;上采样\u0026rdquo;，因为它是从源时间框架转换到更大的时间框架（如从天到周）。\nBacktrader 内置了通过过滤器对象进行重采样的支持。有几种实现方式，但最简单的接口是使用 resampledata 代替 cerebro.adddata(data)。\ncerebro.resampledata(data, **kwargs) 有两个主要选项可以控制：\n更改时间框架 压缩条数 要实现这些功能，请在调用resampledata时使用以下参数：\ntimeframe（默认值：bt.TimeFrame.Days）：目标时间框架，必须等于或大于源时间框架。 compression（默认值：1）：将选定的值“n”压缩为1个条。 让我们来看一个从每日到每周的示例，通过手工编写的脚本：\n$ ./resampling-example.py --timeframe weekly --compression 1 我们可以将其与原始每日数据进行比较：\n$ ./resampling-example.py --timeframe daily --compression 1 实现步骤如下：\n先用 cerebro.adddata 加载原始数据； 用带 timeframe 和 compression 参数的 resampledata 将数据传递给 cerebro； 示例代码：\n# 加载数据 datapath = args.dataname or \u0026#39;../../datas/2006-day-001.txt\u0026#39; data = btfeeds.BacktraderCSVData(dataname=datapath) # 方便的字典用于时间框架参数转换 tframes = dict( daily=bt.TimeFrame.Days, weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) # 添加重采样数据而不是原始数据 cerebro.resampledata(data, timeframe=tframes[args.timeframe], compression=args.compression) 假设将时间框架从每日改为每周，且将 3 条压缩为 1 条。\n$ ./resampling-example.py --timeframe weekly --compression 3 从原始的 256 个每日 Bar 中，最终得到 18 个 3 周的 Bar。因为一年是 52 周，而 52 / 3 = 17.33，因此有18个 Bar。\n重采样过滤器支持其他参数，在大多数情况下不需要更改：\n参数名 默认值 描述 bar2edge True 使用时间边界作为目标。例如，对于“ticks -\u0026gt; 5 seconds”，生成的5秒条将对齐到xx:00、xx:05、xx:10…… adjbartime True 使用边界的时间调整重采样条的时间，而不是最后看到的时间戳。例如，对于重采样到“5 seconds”，条的时间将调整为hh:MM:05，即使最后看到的时间戳是hh:MM:04.33。注意： 只有当 bar2edge 为True时，才会调整时间。如果条没有对齐到边界，调整时间没有意义。 rightedge True 使用时间边界的右边缘设置时间。- 如果为False，并且压缩到5秒，重采样条的时间将在hh:MM:00和hh:MM:04之间。 -如果为True，使用的时间边界为hh:MM:05。 boundoff 0 将重采样/重放边界前移一个单位。例如，从1分钟重采样到15分钟，默认行为是从00:01:00到00:15:00生成一个15分钟重放/重采样条。如果boundoff设置为1，则边界向前推1个单位。在这种情况下，原始单位是1分钟条。因此，重采样/重放将使用00:00:00到00:14:00的条生成15分钟条。 重采样测试脚本示例代码：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import backtrader as bt import backtrader.feeds as btfeeds def runstrat(): args = parse_args() # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) # 添加策略 cerebro.addstrategy(bt.Strategy) # 加载数据 datapath = args.dataname or \u0026#39;../../datas/2006-day-001.txt\u0026#39; data = btfeeds.BacktraderCSVData(dataname=datapath) # 方便的字典用于时间框架参数转换 tframes = dict( daily=bt.TimeFrame.Days, weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) # 添加重采样数据而不是原始数据 cerebro.resampledata(data, timeframe=tframes[args.timeframe], compression=args.compression) # 运行所有内容 cerebro.run() # 绘制结果 cerebro.plot(style=\u0026#39;bar\u0026#39;) def parse_args(): parser = argparse.ArgumentParser( description=\u0026#39;Pandas test script\u0026#39;) parser.add_argument(\u0026#39;--dataname\u0026#39;, default=\u0026#39;\u0026#39;, required=False, help=\u0026#39;要加载的文件数据\u0026#39;) parser.add_argument(\u0026#39;--timeframe\u0026#39;, default=\u0026#39;weekly\u0026#39;, required=False, choices=[\u0026#39;daily\u0026#39;, \u0026#39;weekly\u0026#39;, \u0026#39;monthly\u0026#39;], help=\u0026#39;要重采样到的时间框架\u0026#39;) parser.add_argument(\u0026#39;--compression\u0026#39;, default=1, required=False, type=int, help=\u0026#39;将n个条压缩为1个\u0026#39;) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/06-datafeed-resampling/","section":"教程","summary":"当只有单一时间框架的数据可用，而分析需要在不同时间框架上进行时，就需要重采样。“重采样” 实际应称为 “上采样”，因为它是从源时间框架转换到更大的时间框架（如从天到周）。\n","title":"数据重采样与周期转换","type":"docs"},{"content":"设计目标之一是尽早退出，让用户清楚地了解错误发生的位置。这迫使我们在异常情况下编写会中断的代码，从而必须重新审视受影响的部分。\n但时机已到，平台开始逐步引入一些异常类型。\n继承层次结构 # 所有异常的基类是 BacktraderError（直接继承自 Exception）。\n位置 # 在 errors 模块内，可以通过以下方式访问：\nimport backtrader as bt class Strategy(bt.Strategy): def __init__(self): if something_goes_wrong(): raise bt.errors.StrategySkipError 或者直接从 backtrader 访问：\nimport backtrader as bt class Strategy(bt.Strategy): def __init__(self): if something_goes_wrong(): raise bt.StrategySkipError 异常 # StrategySkipError，请求平台跳过该策略的回测。应在策略实例的 __init__ 阶段抛出。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/05-cerebro/05-exceptions/","section":"教程","summary":"设计目标之一是尽早退出，让用户清楚地了解错误发生的位置。这迫使我们在异常情况下编写会中断的代码，从而必须重新审视受影响的部分。\n但时机已到，平台开始逐步引入一些异常类型。\n","title":"异常处理与错误管理","type":"docs"},{"content":"在传统意义上，GUI程序使用回调来自定义控件的操作。Fyne不暴露插入自定义回调来捕获控件上的事件，但这并不是必需的。Go语言完全有足够的扩展性来实现这一点。\n我们可以简单地使用类型嵌入来扩展控件，使其只能输入数值。\n首先创建一个新的类型结构体，我们将其称为numericalEntry。\ntype numericalEntry struct { widget.Entry } 如扩展现有控件中所提到的，我们遵循良好实践并创建一个构造函数，该函数扩展了BaseWidget。\nfunc newNumericalEntry() *numericalEntry { entry := \u0026amp;numericalEntry{} entry.ExtendBaseWidget(entry) return entry } 现在我们需要让条目只接受数字。这可以通过重写TypedRune(rune)方法来完成，这是fyne.Focusable接口的一部分。这将允许我们拦截按键输入的标准处理，并只通过我们想要的输入。在此方法中，我们将使用条件检查rune是否匹配0到9之间的任何数字。如果是，我们将其委托给嵌入式条目的标准TypedRune(rune)方法。如果不是，我们就忽略输入。此实现只允许输入整数，但如果需要，可以轻松扩展以检查将来的其他键。\nfunc (e *numericalEntry) TypedRune(r rune) { if r \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; r \u0026lt;= \u0026#39;9\u0026#39; { e.Entry.TypedRune(r) } } 如果我们想要更新实现以允许输入小数，我们可以简单地将.和,添加到允许的rune列表中（一些语言对于小数记数使用逗号而不是点）。\nfunc (e *numericalEntry) TypedRune(r rune) { if (r \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; r \u0026lt;= \u0026#39;9\u0026#39;) || r == \u0026#39;.\u0026#39; || r == \u0026#39;,\u0026#39; { e.Entry.TypedRune(r) } } 通过这种方式，现在条目只允许用户在按键时输入数值。然而，粘贴快捷键仍然允许输入文本。为了解决这个问题，我们可以重写TypedShortcut(fyne.Shortcut)方法，这是fyne.Shortcutable接口的一部分。首先我们需要进行类型断言，检查给定的快捷键是否为*fyne.ShortcutPaste类型。如果不是，我们可以将快捷键委托回嵌入式条目。如果是，我们检查剪贴板内容是否为数值，通过使用strconv.ParseFloat()（如果你只想允许整数，strconv.Atoi()就足够了），然后如果剪贴板内容可以无误地解析，再将快捷键委托回嵌入式条目。\nfunc (e *numericalEntry) TypedShortcut(shortcut fyne.Shortcut) { paste, ok := shortcut.(*fyne.ShortcutPaste) if !ok { e.Entry.TypedShortcut(shortcut) return } content := paste.Clipboard.Content() if _, err := strconv.ParseFloat(content, 64); err == nil { e.Entry.TypedShortcut(shortcut) } } 作为额外福利，我们还可以确保移动操作系统打开数值键盘而不是默认键盘。这可以通过首先导入fyne.io/fyne/v2/driver/mobile包并重写mobile.Keyboardable接口的Keyboard() mobile.KeyboardType方法来完成。在函数中，我们简单地返回mobile.NumberKeyboard类型。\nfunc (e *numericalEntry) Keyboard() mobile.KeyboardType { return mobile.NumberKeyboard } 最后，结果代码可能如下所示：\npackage main import ( \u0026#34;strconv\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/driver/mobile\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) type numericalEntry struct { widget.Entry } func newNumericalEntry() *numericalEntry { entry := \u0026amp;numericalEntry{} entry.ExtendBaseWidget(entry) return entry } func (e *numericalEntry) TypedRune(r rune) { if (r \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; r \u0026lt;= \u0026#39;9\u0026#39;) || r == \u0026#39;.\u0026#39; || r == \u0026#39;,\u0026#39; { e.Entry.TypedRune(r) } } func (e *numericalEntry) TypedShortcut(shortcut fyne.Shortcut) { paste, ok := shortcut.(*fyne.ShortcutPaste) if !ok { e.Entry.TypedShortcut(shortcut) return } content := paste.Clipboard.Content() if _, err := strconv.ParseFloat(content, 64); err == nil { e.Entry.TypedShortcut(shortcut) } } func (e *numericalEntry) Keyboard() mobile.KeyboardType { return mobile.NumberKeyboard } func main() { a := app.New() w := a.NewWindow(\u0026#34;Numerical\u0026#34;) entry := newNumericalEntry() w.SetContent(entry) w.ShowAndRun() } 这个示例展示了如何扩展Fyne的Entry控件来创建一个只接受数值输入的Entry控件，通过重写TypedRune和TypedShortcut方法，并通过重写Keyboard方法为移动设备提供了数值键盘。\n","date":"2025-05-05","externalUrl":null,"permalink":"/docs/gofyne/08-extend/06-numerical-entry/","section":"教程","summary":"在传统意义上，GUI程序使用回调来自定义控件的操作。Fyne不暴露插入自定义回调来捕获控件上的事件，但这并不是必需的。Go语言完全有足够的扩展性来实现这一点。\n","title":"数字输入框 Entry","type":"docs"},{"content":"","date":"2025-03-18","externalUrl":null,"permalink":"/docs/gofyne/06-collection/","section":"教程","summary":"","title":"集合","type":"docs"},{"content":"进度条控件有两种形式，标准进度条向用户显示已达到的 Value，从 Min 到 Max。默认最小值是 0.0，最大值默认为 1.0。要使用默认值，只需调用 widget.NewProgressBar()。创建后，你可以设置 Value 字段。\n要设置自定义范围，你可以手动设置 Min 和 Max 字段。标签将始终显示完成百分比。\n进度控件的另一种形式是无限进度条。此版本仅通过将条的一部分从左向右移动然后再移动回来，简单地显示一些活动正在进行中。使用 widget.NewProgressBarInfinite() 创建此版本，并且一旦显示就会开始动画。\npackage main import ( \u0026#34;time\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;进度条控件\u0026#34;) progress := widget.NewProgressBar() infinite := widget.NewProgressBarInfinite() go func() { for i := 0.0; i \u0026lt;= 1.0; i += 0.1 { time.Sleep(time.Millisecond * 250) progress.SetValue(i) } }() myWindow.SetContent(container.NewVBox(progress, infinite)) myWindow.ShowAndRun() } ","date":"2025-03-12","externalUrl":null,"permalink":"/docs/gofyne/05-widget/06-progressbar/","section":"教程","summary":"进度条控件有两种形式，标准进度条向用户显示已达到的 Value，从 Min 到 Max。默认最小值是 0.0，最大值默认为 1.0。要使用默认值，只需调用 widget.NewProgressBar()。创建后，你可以设置 Value 字段。\n","title":"进度条 ProgressBar","type":"docs"},{"content":"layout.CenterLayout 将其容器中的所有项目都组织在可用空间的中心。这些对象将按照它们被传递到容器的顺序绘制，最后传递的对象将被绘制在最顶部。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/layout\u0026#34; \u0026#34;fyne.io/fyne/v2/theme\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Center Layout\u0026#34;) img := canvas.NewImageFromResource(theme.FyneLogo()) img.FillMode = canvas.ImageFillOriginal text := canvas.NewText(\u0026#34;Overlay\u0026#34;, color.Black) content := container.New(layout.NewCenterLayout(), img, text) myWindow.SetContent(content) myWindow.ShowAndRun() } 中心布局使所有项目保持其最小大小，如果你希望扩展项目以填充空间，那么请参见layout.MaxLayout。\n","date":"2025-02-13","externalUrl":null,"permalink":"/docs/gofyne/04-container/06-center/","section":"教程","summary":"layout.CenterLayout 将其容器中的所有项目都组织在可用空间的中心。这些对象将按照它们被传递到容器的顺序绘制，最后传递的对象将被绘制在最顶部。\npackage main import ( \"image/color\" \"fyne.io/fyne/v2/app\" \"fyne.io/fyne/v2/canvas\" \"fyne.io/fyne/v2/container\" \"fyne.io/fyne/v2/layout\" \"fyne.io/fyne/v2/theme\" ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\"Center Layout\") img := canvas.NewImageFromResource(theme.FyneLogo()) img.FillMode = canvas.ImageFillOriginal text := canvas.NewText(\"Overlay\", color.Black) content := container.New(layout.NewCenterLayout(), img, text) myWindow.SetContent(content) myWindow.ShowAndRun() } 中心布局使所有项目保持其最小大小，如果你希望扩展项目以填充空间，那么请参见layout.MaxLayout。\n","title":"居中布局 Center","type":"docs"},{"content":"canvas.Raster 类似于图像，但在屏幕上为每个像素精确绘制一个点。这意味着，随着用户界面缩放或图像调整大小，将请求更多像素来填充空间。为此，我们使用一个 Generator 函数，如此示例所示——它将用于返回每个像素的颜色。\n生成器函数可以基于像素（如此示例中我们为每个像素生成一个新的随机颜色）或基于完整图像。生成完整图像（使用 canvas.NewRaster()）更高效，但有时直接控制像素更方便。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;Raster\u0026#34;) raster := canvas.NewRasterWithPixels( func(_, _, w, h int) color.Color { return color.RGBA{R: uint8(rand.Intn(255)), G: uint8(rand.Intn(255)), B: uint8(rand.Intn(255)), A: 0xff} }) // raster := canvas.NewRasterFromImage() w.SetContent(raster) w.Resize(fyne.NewSize(120, 100)) w.ShowAndRun() } 如果您的像素数据存储在图像中，您可以通过 NewRasterFromImage() 函数加载它，该函数将加载图像以在屏幕上精确显示像素。\n","date":"2025-01-17","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/06-raster/","section":"教程","summary":"canvas.Raster 类似于图像，但在屏幕上为每个像素精确绘制一个点。这意味着，随着用户界面缩放或图像调整大小，将请求更多像素来填充空间。为此，我们使用一个 Generator 函数，如此示例所示——它将用于返回每个像素的颜色。\n","title":"矢量 Raster","type":"docs"},{"content":"以下每个图标都可以通过theme包作为一个函数获得。例如theme.InfoIcon()。\n这些图标也可以通过使用ThemeIconName以及在实现了fyne.Theme的结构体上的Icon方法，通过它们的源图标名称获得。例如theme.Icon(theme.IconNameInfo)。\n列表 # Fyne 提供丰富的内置图标，完整列表请查看 Fyne 官方图标文档。\n以下是常用图标的调用方式：\ntheme.InfoIcon() theme.AccountIcon() theme.ArrowDropDownIcon() theme.ArrowDropUpIcon() 以及更多\u0026hellip; 所有图标都可通过 theme.Icon(theme.IconNameXxx) 的方式引用。 使用其他颜色集 # 每个图标都可以作为特定主题颜色的源使用各种公共帮助方法：\nNewDisabledThemedResource NewErrorThemedResource NewInvertedThemedResource NewPrimaryThemedResource 默认情况下，所有图标都适应当前主题前景色，使用NewThemedResource，它使用主题前景色。所有图标都是SVG width=\u0026quot;24\u0026quot;, height=\u0026quot;24\u0026quot;。\n","date":"2024-12-12","externalUrl":null,"permalink":"/docs/gofyne/02-explore/06-icons/","section":"教程","summary":"以下每个图标都可以通过theme包作为一个函数获得。例如theme.InfoIcon()。\n这些图标也可以通过使用ThemeIconName以及在实现了fyne.Theme的结构体上的Icon方法，通过它们的源图标名称获得。例如theme.Icon(theme.IconNameInfo)。\n","title":"主题 Icon 小图标","type":"docs"},{"content":"窗口是使用App.NewWindow()创建的，并需要使用Show()函数来显示。fyne.Window上的辅助方法ShowAndRun()允许你同时显示窗口并运行应用程序。\n默认情况下，窗口将通过检查MinSize()函数（在后面的示例中会有更多介绍）来显示其内容的正确大小。你可以通过调用Window.Resize()方法来设置更大的尺寸。这个方法接受一个fyne.Size，其中包含使用设备独立像素（这意味着在不同设备上将是相同的）的宽度和高度，例如，要默认使窗口正方形，我们可以这样做：\nw.Resize(fyne.NewSize(100, 100)) 请注意，桌面环境可能有限制，导致窗口小于请求的尺寸。移动设备通常会忽略这一点，因为它们只以全屏显示。\n如果你希望显示第二个窗口，你只需调用Show()函数。如果你想在应用程序启动时打开多个窗口，将Window.Show()与App.Run()分开也可能是有帮助的。下面的示例展示了如何在启动时加载两个窗口。\npackage main import ( \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;Hello World\u0026#34;) w.SetContent(widget.NewLabel(\u0026#34;Hello World!\u0026#34;)) w.Show() w2 := a.NewWindow(\u0026#34;Larger\u0026#34;) w2.SetContent(widget.NewLabel(\u0026#34;More content\u0026#34;)) w2.Resize(fyne.NewSize(100, 100)) w2.Show() a.Run() } 上述应用程序将在两个窗口都关闭时退出。如果你的应用程序安排得当，一个窗口是主窗口，其他窗口是辅助视图，你可以设置一个窗口为“主窗口”，这样如果该窗口关闭，应用程序就会退出。要做到这一点，使用Window上的SetMaster()函数。\n窗口可以在任何时候被创建，我们可以改变上面的代码，使得第二个窗口（w2）的内容是一个打开新窗口的按钮。你也可以从更复杂的工作流中加载窗口，但要小心，因为新窗口通常会出现在当前活动内容之上。\nw2.SetContent(widget.NewButton(\u0026#34;Open new\u0026#34;, func() { w3 := a.NewWindow(\u0026#34;Third\u0026#34;) w3.SetContent(widget.NewLabel(\u0026#34;Third\u0026#34;)) w3.Show() })) 这段描述说明了如何在Fyne应用程序中处理窗口，包括创建、显示、调整大小以及如何从代码中动态添加新窗口。\n","date":"2024-10-31","externalUrl":null,"permalink":"/docs/gofyne/01-started/06-windows/","section":"教程","summary":"窗口是使用App.NewWindow()创建的，并需要使用Show()函数来显示。fyne.Window上的辅助方法ShowAndRun()允许你同时显示窗口并运行应用程序。\n","title":"窗口 Window 处理","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/","section":"教程","summary":"","title":"DataFeed","type":"docs"},{"content":"Backtrader 引入了一个独特的概念——Line 迭代器（Line Iterator）。其核心思想是通过迭代数据来驱动策略和指标的运作。它和 Python 的普通迭代器有些表面相似，但专为金融数据处理量身定制。\n在 Backtrader 中，策略和指标都是基于 Line 迭代器构建的。下面逐步拆解这个概念。\n什么是 Line 迭代器？ # Line 迭代器是控制”数据处理节奏”的工具，主要职责是：\n驱动数据流动：像”指挥者”一样，触发从属 Line 迭代器（如指标或策略）依次处理数据。 逐步更新数据：按照声明的规则迭代数据，并在每一步设置对应的结果。 Line 迭代器如何工作？ # 三大关键方法 # prenext\n数据不足以完成计算时调用。 用于初始化阶段的数据处理，比如累加数据。 nextstart\n数据累积到”最小周期”时调用，仅触发一次。 默认会调用 next 方法。 next\n每次迭代时调用，用于正式处理当前索引上的数据。 为什么需要这些方法？ # 为生成有效的计算结果，某些指标需要一个”缓冲期”。如 25 周期的简单移动平均线 (SMA) 需要累积 25 个数据点才能生成第一个值。在此之前，用 prenext 处理空白期。\n一旦累积到足够的数据点，进入“正式运行”阶段后，next 方法会被不断调用，每次处理新到达的数据。\n示例：如何实现一个简单的 SMA # 以下是一个 SimpleMovingAverage（简单移动平均线）的实现示例：\nclass SimpleMovingAverage(Indicator): lines = (\u0026#39;sma\u0026#39;,) params = dict(period=25) def prenext(self): print(f\u0026#39;prenext:: 当前周期: {len(self)}\u0026#39;) def nextstart(self): print(f\u0026#39;nextstart:: 当前周期: {len(self)}\u0026#39;) self.next() # 模拟默认行为 def next(self): print(f\u0026#39;next:: 当前周期: {len(self)}\u0026#39;) 实例化 SMA 的过程 # 假设我们为一个数据集创建一个 SimpleMovingAverage 指标：\nsma = btind.SimpleMovingAverage(self.data, period=25) SMA 的调用流程 # prenext：\n前 24 次调用。 在指标数据不足 25 个点时，每次新数据到达时调用，用于累积数据。 nextstart：\n第 25 次调用。 数据点达到 25 时，开始生成第一个有效的 SMA 值。 next：\n从第 26 次开始，每次新数据到达时调用。 多层指标的交互 # 当一个指标的输出作为另一个指标的输入时，会发生什么？例如：\nsma1 = btind.SimpleMovingAverage(self.data, period=25) sma2 = btind.SimpleMovingAverage(sma1, period=20) 调用流程 # sma1 的最小周期是 25：\n需要 25 个数据点后才能生成第一个有效值。 在此之前，sma1 的 prenext 被调用 24 次。 sma2 的最小周期是 sma1 的 25 加上自己的 20：\nsma2 的 prenext 被调用 44 次。 第 45 次调用时，sma2 的 nextstart 被触发，并开始生成第一个有效值。 因此，sma2 的计算需要至少 45 个数据点才能开始正常工作。\n指标的性能优化：runonce 模式 # 为提高性能，Backtrader 提供了批量处理模式——runonce。它可以一次性处理多个数据点，而不是逐步调用 next 方法。\nrunonce 的核心方法 # once(self, start, end)\n达到最小周期时调用，批量处理 start 到 end 索引范围内的数据。 preonce(self, start, end)\n类似于 prenext，但在批量模式下调用。 oncestart(self, start, end)\n类似于 nextstart，但用于批量模式。 批量处理模式的优势在于减少不必要的函数调用，显著提高性能。\n最小周期的意义 # 最小周期是 Line 迭代器中一个关键的控制点，决定了何时开始生成有意义的输出值。例如：\n对于一个 25 周期的 SMA，最小周期是 25。 当一个指标依赖另一个指标作为输入时，最小周期会累积。 在多层指标中，最小周期的自动调整可以确保所有数据都有意义。\nLine 迭代器的核心价值 # 控制节奏：Line 迭代器通过 prenext、nextstart 和 next 方法灵活控制数据处理节奏。 支持复杂数据流：允许多个指标相互依赖，同时自动调整最小周期。 性能优化：通过 runonce 模式，指标的批量处理显著提升了效率。 简单来说，Line 迭代器是 Backtrader 中的强大工具，它让平台能够高效处理多层次的指标和策略。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/06-iteration/","section":"教程","summary":"Backtrader 引入了一个独特的概念——Line 迭代器（Line Iterator）。其核心思想是通过迭代数据来驱动策略和指标的运作。它和 Python 的普通迭代器有些表面相似，但专为金融数据处理量身定制。\n","title":"Line 迭代器与循环机制","type":"docs"},{"content":"仅对已完成的 Bar 进行策略测试已不够，数据回放应运而生。假设策略在时间框架 X（如每日）上操作，而数据在更小的时间框架 Y（如 1 分钟）上可用。\n数据回放正如其名，使用 1 分钟数据来重建每日 Bar。虽然不能完全再现市场发展，但比观察已完成的每日 Bar 要好得多。这可以近似模拟策略在每日 Bar 形成过程中的实际表现。\n实现数据回放，按常规使用 backtrader 即可。\n加载数据源 用 replaydata 将数据传递给 cerebro 添加策略 注意： 数据回放不支持预加载，因为每个 Bar 实际上是实时构建的，任何 Cerebro 实例中都会自动禁用预加载。\n可传递给replaydata的参数：\n参数 默认值 描述 timeframe bt.TimeFrame.Days 目标时间框架，必须等于或大于源时间框架 compression 1 将选定值“n”压缩为1条 扩展参数（若无特别需要请勿修改）：\n参数 默认值 描述 bar2edge True 使用时间边界作为闭合条的目标。例如，使用“ticks -\u0026gt; 5 seconds”时，生成的5秒条将对齐到xx:00、xx:05、xx:10…… adjbartime False 使用边界的时间调整传递的重采样条的时间，而不是最后看到的时间戳。 rightedge True 使用时间边界的右边缘设置时间。 举例说明，标准的 2006 年每日数据按每周进行回放。\n最终有 52 个 Bar，每周一个； Cerebro 会调用 prenext 和 next 共计 255 次（原始每日 Bar 的数量）； 关键点：\n每周 Bar 形成过程中，策略长度（len(self)）保持不变。 每周结束后，长度增加 1。 以下是测试脚本的主要部分，加载数据并传递给 cerebro 进行回放：\n# 加载数据 datapath = args.dataname or \u0026#39;../../datas/2006-day-001.txt\u0026#39; data = btfeeds.BacktraderCSVData(dataname=datapath) # 方便的字典用于时间框架参数转换 tframes = dict( daily=bt.TimeFrame.Days, weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) # 首先添加原始数据 - 较小的时间框架 cerebro.replaydata(data, timeframe=tframes[args.timeframe], compression=args.compression) 示例 - 每日回放至每周 # 脚本调用：\n$ ./replay-example.py --timeframe weekly --compression 1 图表无法显示后台的实际过程，下面看一下控制台输出：\nprenext len 1 - counter 1 prenext len 1 - counter 2 prenext len 1 - counter 3 prenext len 1 - counter 4 prenext len 1 - counter 5 prenext len 2 - counter 6 ... prenext len 9 - counter 44 prenext len 9 - counter 45 ---next len 10 - counter 46 ---next len 10 - counter 47 ---next len 10 - counter 48 ---next len 10 - counter 49 ---next len 10 - counter 50 ---next len 11 - counter 51 ---next len 11 - counter 52 ---next len 11 - counter 53 ... ---next len 51 - counter 248 ---next len 51 - counter 249 ---next len 51 - counter 250 ---next len 51 - counter 251 ---next len 51 - counter 252 ---next len 52 - counter 253 ---next len 52 - counter 254 ---next len 52 - counter 255 内部的 self.counter 变量跟踪每次 prenext 或 next 调用。前者在 SMA 产生值之前调用，后者在 SMA 产生值之后调用。\n策略长度（len(self)）每 5 条（每周 5 个交易日）变化一次。 策略实际看到的是每周 Bar 在 5 次更新中的形成过程。 这不能完全再现市场的逐秒发展，但比仅观察完成的 Bar 要好。\n最终的视觉输出是每周图表。\n示例2 - 每日压缩 # “回放”也可以应用于相同时间框架加上压缩。\n控制台：\n$ ./replay-example.py --timeframe daily --compression 2 prenext len 1 - counter 1 prenext len 1 - counter 2 prenext len 2 - counter 3 prenext len 2 - counter 4 prenext len 3 - counter 5 prenext len 3 - counter 6 prenext len 4 - counter 7 ... ---next len 125 - counter 250 ---next len 126 - counter 251 ---next len 126 - counter 252 ---next len 127 - counter 253 ---next len 127 - counter 254 ---next len 128 - counter 255 这次我们得到了预期的一半条数，因为请求了2倍压缩。\n结论 # 可以利用更小时间框架的数据，离散地重建系统操作时间框架内的市场发展。\n测试脚本如下：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class SMAStrategy(bt.Strategy): params = ( (\u0026#39;period\u0026#39;, 10), (\u0026#39;onlydaily\u0026#39;, False), ) def __init__(self): self.sma = btind.SMA(self.data, period=self.p.period) def start(self): self.counter = 0 def prenext(self): self.counter += 1 print(\u0026#39;prenext len %d - counter %d\u0026#39; % (len(self), self.counter)) def next(self): self.counter += 1 print(\u0026#39;---next len %d - counter %d\u0026#39; % (len(self), self.counter)) def runstrat(): args = parse_args() # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) cerebro.addstrategy( SMAStrategy, # 策略参数 period=args.period, ) # 加载数据 datapath = args.dataname or \u0026#39;../../datas/2006-day-001.txt\u0026#39; data = btfeeds.BacktraderCSVData(dataname=datapath) # 方便的字典用于时间框架参数转换 tframes = dict( daily=bt.TimeFrame.Days, weekly=bt.TimeFrame.Weeks, monthly=bt.TimeFrame.Months) # 首先添加原始数据 - 较小的时间框架 cerebro.replaydata(data, timeframe=tframes[args.timeframe], compression=args.compression) # 运行所有内容 cerebro.run() # 绘制结果 cerebro.plot(style=\u0026#39;bar\u0026#39;) def parse_args(): parser = argparse.ArgumentParser( description=\u0026#39;Pandas test script\u0026#39;) parser.add_argument(\u0026#39;--dataname\u0026#39;, default=\u0026#39;\u0026#39;, required=False, help=\u0026#39;要加载的文件数据\u0026#39;) parser.add_argument(\u0026#39;--timeframe\u0026#39;, default=\u0026#39;weekly\u0026#39;, required=False, choices=[\u0026#39;daily\u0026#39;, \u0026#39;weekly\u0026#39;, \u0026#39;monthly\u0026#39;], help=\u0026#39;要重采样到的时间框架\u0026#39;) parser.add_argument(\u0026#39;--compression\u0026#39;, default=1, required=False, type=int, help=\u0026#39;将n个条压缩为1个\u0026#39;) parser.add_argument(\u0026#39;--period\u0026#39;, default=10, required=False, type=int, help=\u0026#39;应用于指标的周期\u0026#39;) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/07-datafeed-replay/","section":"教程","summary":"仅对已完成的 Bar 进行策略测试已不够，数据回放应运而生。假设策略在时间框架 X（如每日）上操作，而数据在更小的时间框架 Y（如 1 分钟）上可用。\n数据回放正如其名，使用 1 分钟数据来重建每日 Bar。虽然不能完全再现市场发展，但比观察已完成的每日 Bar 要好得多。这可以近似模拟策略在每日 Bar 形成过程中的实际表现。\n","title":"Tick 级数据回放机制","type":"docs"},{"content":" 本节演示一个简单策略：如果出现连续两个交易日下跌，则执行买入操作。\n基于上节的策略类 TestStrategy 继续开发，策略逻辑在 next 方法中实现。\nclass TestStrategy(bt.Strategy): def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) 如何判断两日连续下跌？简单来说就是 close[0] \u0026lt; close[-1] 且 close[-1] \u0026lt; close[-2]，即当前收盘价小于昨日收盘价，昨日收盘价小于前日收盘价。\nself.dataclose[0] \u0026lt; self.dataclose[-1] and self.dataclose[-1] \u0026lt; self.dataclose[-2] 买入操作使用 self.buy() 即可：\nself.buy() 默认情况下，self.buy 买入的是第一个数据源的资产。\n完整示例 # import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) import backtrader as bt class TestStrategy(bt.Strategy): def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) if self.dataclose[0] \u0026lt; self.dataclose[-1]: if self.dataclose[-1] \u0026lt; self.dataclose[-2]: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.dataclose[0]) self.buy() if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() cerebro.addstrategy(TestStrategy) modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, \u0026#39;./orcl-1995-2014.txt\u0026#39;) data = bt.feeds.YahooFinanceCSVData( dataname=datapath, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), reverse=False) cerebro.adddata(data) cerebro.broker.setcash(100000.0) print(\u0026#39;Starting Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) cerebro.run() print(\u0026#39;Final Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) 输出：\nStarting Portfolio Value: 100000.00 2000-01-03, Close, 27.85 2000-01-04, Close, 25.39 2000-01-05, Close, 24.05 2000-01-05, BUY CREATE, 24.05 2000-01-06, Close, 22.63 2000-01-06, BUY CREATE, 22.63 2000-01-07, Close, 24.37 ... ... ... 2000-12-20, BUY CREATE, 26.88 2000-12-21, Close, 27.82 2000-12-22, Close, 30.06 2000-12-26, Close, 29.17 2000-12-27, Close, 28.94 2000-12-27, BUY CREATE, 28.94 2000-12-28, Close, 29.29 2000-12-29, Close, 27.41 Final Portfolio Value: 99725.08 多个买入订单被发出，组合价值减少了。显然缺少了重要环节：订单已创建，但尚未确定执行时间和价格。\n下个示例将通过监听订单状态通知来完善这个过程。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/05-strategy-logic/","section":"教程","summary":" 本节演示一个简单策略：如果出现连续两个交易日下跌，则执行买入操作。\n基于上节的策略类 TestStrategy 继续开发，策略逻辑在 next 方法中实现。\n","title":"策略中的买卖逻辑","type":"docs"},{"content":"版本 1.9.35.116 在回测工具中增加了跟踪止损和跟踪止损限价订单执行类型。\n注意，最初只在回测中实现。1.9.36.116 版本起，Interactive Brokers 也支持跟踪止损、跟踪止损限价和 OCO。\nOCO 始终将组中第一个订单指定为 oco 参数。\n跟踪止损限价：回测 Broker 和 IB Broker 行为一致。指定 price 作为初始止损触发价格（同时指定 trailamount），plimit 作为初始限价。两者差值决定 limitoffset（限价与止损触发价格之间的距离）。\n使用模式已集成到策略的 buy、sell 和 close 方法中，只需：\n指定执行类型，如 exectype=bt.Order.StopTrail 设置跟踪距离：trailamount=10（固定距离）或 trailpercent=0.02（2% 百分比距离） 多头场景（先买入，后以 StopTrail 卖出）：\n若未指定价格，使用最新收盘价。 从价格中减去 trailamount 得到止损（触发）价格。 Broker 每次迭代检查是否触及触发价格： 是：订单以市价方式执行。 否：使用最新收盘价重新计算止损价格（减去 trailamount）。 如果新价格更高，止损价格上移；如果新价格更低（或不变），止损价格保持不变。 也就是说：止损价格随价格上涨而跟随，价格下跌时保持不变，以锁定利润。\n空头场景（先卖出，后以 StopTrail 买入）：逻辑相反，价格向下跟随。\n一些使用模式\n# 对于向下的跟踪止损 # 将使用最后价格作为参考 self.buy(size=1, exectype=bt.Order.StopTrail, trailamount=0.25) # 或 self.buy(size=1, exectype=bt.Order.StopTrail, price=10.50, trailamount=0.25) # 对于向上的跟踪止损 # 将使用最后价格作为参考 self.sell(size=1, exectype=bt.Order.StopTrail, trailamount=0.25) # 或 self.sell(size=1, exectype=bt.Order.StopTrail, price=10.50, trailamount=0.25) 也可以指定 trailpercent 而不是 trailamount，并将距离价格的距离计算为价格的百分比\n# 对于 2% 距离的向下跟踪止损 # 将使用最后价格作为参考 self.buy(size=1, exectype=bt.Order.StopTrail, trailpercent=0.02) # 或 self.buy(size=1, exectype=bt.Order.StopTrail, price=10.50, trailpercent=0.02) # 对于 2% 距离的向上跟踪止损 # 将使用最后价格作为参考 self.sell(size=1, exectype=bt.Order.StopTrail, trailpercent=0.02) # 或 self.sell(size=1, exectype=bt.Order.StopTrail, price=10.50, trailpercent=0.02) 对于跟踪止损限价\n唯一的区别在于触发跟踪止损价格时发生的事情。\n在这种情况下，订单作为限价订单执行（与 StopLimit 订单具有相同的行为，但此时触发价格是动态的）\n注意：必须指定 plimit=x.x 来买入或卖出，这将是限价\n注意：限价不会像止损/触发价格那样动态变化\n示例胜过千言万语，这里有一个 backtrader 的常规示例：\n使用均线向上交叉进入多头 使用跟踪止损退出 执行时固定价格距离为 50 点\n$ ./trail.py --plot --strat trailamount=50.0 以及以下输出：\n************************************************** 2005-02-14,3075.76,3025.76,3025.76 ---------- 2005-02-15,3086.95,3036.95,3036.95 2005-02-16,3068.55,3036.95,3018.55 2005-02-17,3067.34,3036.95,3017.34 2005-02-18,3072.04,3036.95,3022.04 2005-02-21,3063.64,3036.95,3013.64 ... ... ************************************************** 2005-05-19,3051.79,3001.79,3001.79 ---------- 2005-05-20,3050.45,3001.79,3000.45 2005-05-23,3070.98,3020.98,3020.98 2005-05-24,3066.55,3020.98,3016.55 2005-05-25,3059.84,3020.98,3009.84 2005-05-26,3086.08,3036.08,3036.08 2005-05-27,3084.0,3036.08,3034.0 2005-05-30,3096.54,3046.54,3046.54 2005-05-31,3076.75,3046.54,3026.75 2005-06-01,3125.88,3075.88,3075.88 2005-06-02,3131.03,3081.03,3081.03 2005-06-03,3114.27,3081.03,3064.27 2005-06-06,3099.2,3081.03,3049.2 2005-06-07,3134.82,3084.82,3084.82 2005-06-08,3125.59,3084.82,3075.59 2005-06-09,3122.93,3084.82,3072.93 2005-06-10,3143.85,3093.85,3093.85 2005-06-13,3159.83,3109.83,3109.83 2005-06-14,3162.86,3112.86,3112.86 2005-06-15,3147.55,3112.86,3097.55 2005-06-16,3160.09,3112.86,3110.09 2005-06-17,3178.48,3128.48,3128.48 2005-06-20,3162.14,3128.48,3112.14 2005-06-21,3179.62,3129.62,3129.62 2005-06-22,3182.08,3132.08,3132.08 2005-06-23,3190.8,3140.8,3140.8 2005-06-24,3161.0,3140.8,3111.0 ... ... ... ************************************************** 2006-12-19,4100.48,4050.48,4050.48 ---------- 2006-12-20,4118.54,4068.54,4068.54 2006-12-21,4112.1,4068.54,4062.1 2006-12-22,4073.5,4068.54,4023.5 2006-12-27,4134.86,4084.86,4084.86 2006-12-28,4130.66,4084.86,4080.66 2006-12-29,4119.94,4084.86,4069.94 系统使用跟踪止损退出市场，而不是等待常规的向下交叉信号。以第一次操作为例：\n开仓时的收盘价：3075.76 系统计算的止损价格：3025.76（距离 50 个单位） 输出中每行最后一个价格即为止损价格：3025.76 计算之后：\n收盘价上涨至 3086.95，止损价调整为 3036.95 后续收盘价未超过 3086.95，止损价保持不变 其他两次操作也是同样的模式。\n为了比较，执行时固定距离为 30 点（仅图表）\n$ ./trail.py --plot --strat trailamount=30.0 图表如下：\n最后一次执行 trailpercent=0.02\n$ ./trail.py --plot --strat trailpercent=0.02 示例用法 # $ ./trail.py --help usage: trail.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] StopTrail Sample optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: ) 示例代码 # from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt class St(bt.Strategy): params = dict( ma=bt.ind.SMA, p1=10, p2=30, stoptype=bt.Order.StopTrail, trailamount=0.0, trailpercent=0.0, ) def __init__(self): ma1, ma2 = self.p.ma(period=self.p.p1), self.p.ma(period=self.p.p2) self.crup = bt.ind.CrossUp(ma1, ma2) self.order = None def next(self): if not self.position: if self.crup: o = self.buy() self.order = None print(\u0026#39;*\u0026#39; * 50) elif self.order is None: self.order = self.sell(exectype=self.p.stoptype, trailamount=self.p.trailamount, trailpercent=self.p.trailpercent) if self.p.trailamount: tcheck = self.data.close - self.p.trailamount else: tcheck = self.data.close * (1.0 - self.p.trailpercent) print(\u0026#39;,\u0026#39;.join( map(str, [self.datetime.date(), self.data.close[0], self.order.created.price, tcheck]) ) ) print(\u0026#39;-\u0026#39; * 10) else: if self.p.trailamount: tcheck = self.data.close - self.p.trailamount else: tcheck = self.data.close * (1.0 - self.p.trailpercent) print(\u0026#39;,\u0026#39;.join( map(str, [self.datetime.date(), self.data.close[0], self.order.created.price, tcheck]) ) ) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # Data feed kwargs kwargs = dict() # Parse from/to-date dtfmt, tmfmt = \u0026#39;%Y-%m-%d\u0026#39;, \u0026#39;T%H:%M:%S\u0026#39; for a, d in ((getattr(args, x), x) for x in [\u0026#39;fromdate\u0026#39;, \u0026#39;todate\u0026#39;]): if a: strpfmt = dtfmt + tmfmt * (\u0026#39;T\u0026#39; in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # Data feed data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) # Broker cerebro.broker = bt.brokers.BackBroker(**eval(\u0026#39;dict(\u0026#39; + args.broker + \u0026#39;)\u0026#39;)) # Sizer cerebro.addsizer(bt.sizers.FixedSize, **eval(\u0026#39;dict(\u0026#39; + args.sizer + \u0026#39;)\u0026#39;)) # Strategy cerebro.addstrategy(St, **eval(\u0026#39;dict(\u0026#39; + args.strat + \u0026#39;)\u0026#39;)) # Execute cerebro.run(**eval(\u0026#39;dict(\u0026#39; + args.cerebro + \u0026#39;)\u0026#39;)) if args.plot: # Plot if requested to cerebro.plot(**eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;)) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( \u0026#39;StopTrail Sample\u0026#39; ) ) parser.add_argument(\u0026#39;--data0\u0026#39;, default=\u0026#39;../../datas/2005-2006-day-001.txt\u0026#39;, required=False, help=\u0026#39;Data to read in\u0026#39;) # Defaults for dates parser.add_argument(\u0026#39;--fromdate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--todate\u0026#39;, required=False, default=\u0026#39;\u0026#39;, help=\u0026#39;Date[time] in YYYY-MM-DD[THH:MM:SS] format\u0026#39;) parser.add_argument(\u0026#39;--cerebro\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add_argument(\u0026#39;--broker\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add.argument(\u0026#39;--sizer\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add.argument(\u0026#39;--strat\u0026#39;, required=False, default=\u0026#39;\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) parser.add.argument(\u0026#39;--plot\u0026#39;, required=False, default=\u0026#39;\u0026#39;, nargs=\u0026#39;?\u0026#39;, const=\u0026#39;{}\u0026#39;, metavar=\u0026#39;kwargs\u0026#39;, help=\u0026#39;kwargs in key=value format\u0026#39;) return parser.parse_args(pargs) if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/09-orders/07-stop-trails/","section":"教程","summary":"版本 1.9.35.116 在回测工具中增加了跟踪止损和跟踪止损限价订单执行类型。\n注意，最初只在回测中实现。1.9.36.116 版本起，Interactive Brokers 也支持跟踪止损、跟踪止损限价和 OCO。\n","title":"跟踪止损与限价单","type":"docs"},{"content":"Writer 类负责将以下内容写入输出流：\n数据源、策略、指标和观察者的 CSV 流。可通过每个对象的 csv 属性控制哪些对象输出到 CSV 流（数据源和观察者默认为 True，指标默认为 False）。\n属性摘要：\n数据源 策略（线条和参数） 指标/观察者（线条和参数） 分析器（参数和分析结果） 系统中定义了一个名为 WriterFile 的 Writer，可通过以下方式添加：\n将 Cerebro 的 writer 参数设为 True，会实例化一个标准的 WriterFile 调用 Cerebro.addwriter(writerclass, **kwargs)，在回测执行期间使用给定的 kwargs 实例化 writerclass 由于标准 WriterFile 默认不输出 CSV，可使用如下方式启用：\ncerebro.addwriter(bt.WriterFile, csv=True) 参考 # class backtrader.WriterFile() # 系统范围内的 Writer 类。\n可以通过以下参数进行参数化：\n参数名 默认 说明 out sys.stdout 写入的输出流。如果传递的是字符串，则将其作为文件名。 close_out False 如果 out 是流对象，是否需要由 Writer 显式关闭。 csv False 是否将数据源、策略、观察者和指标的 CSV 流写入输出。可通过每个对象的 csv 属性控制哪些对象输出到 CSV（数据源和观察者默认为 True，指标默认为 False）。 csv_filternan True 在 CSV 输出中清除 NaN 值（替换为空字段）。 csv_counter True 是否记录并输出实际输出行的计数器。 indent 2 每个级别的缩进空格数。 separators '=', '-', '+', '*', '.', '~', '\u0026quot;', '^', '#' 用于分隔部分/子部分的行分隔符字符。 seplen 79 包括缩进在内的行分隔符的总长度。 rounding None 浮点数保留的小数位数。如果为 None，则不做舍入。 ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/05-cerebro/06-logging-writer/","section":"教程","summary":"Writer 类负责将以下内容写入输出流：\n数据源、策略、指标和观察者的 CSV 流。可通过每个对象的 csv 属性控制哪些对象输出到 CSV 流（数据源和观察者默认为 True，指标默认为 False）。\n","title":"日志记录与 Writer 输出","type":"docs"},{"content":"","date":"2025-03-30","externalUrl":null,"permalink":"/docs/gofyne/07-binding/","section":"教程","summary":"","title":"数据绑定","type":"docs"},{"content":"工具栏控件使用图标创建一行动作按钮来表示每个操作。widget.NewToolbar(...) 构造函数接受一系列 widget.ToolbarItem 参数。内置的工具栏项目类型有动作，分隔符和空格器。\n最常用的项目是动作，使用 widget.NewToolbarAction(..) 函数创建。动作有两个参数，第一个是要绘制的图标资源，后一个是点击时调用的 func()。这样创建了一个标准的工具栏按钮。\n你可以使用 widget.NewToolbarSeparator() 在工具栏中的项目之间创建一个小分隔符（通常是一个细的垂直线）。最后，你可以使用 widget.NewToolbarSpacer() 在元素之间创建一个灵活的空间。这最适合用于右对齐列表中的工具栏项目。\n工具栏应始终位于内容区域的顶部，所以通常使用 layout.BorderLayout 将其添加到 fyne.Container 中，以便将其与其他内容对齐。\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/theme\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;工具栏控件\u0026#34;) toolbar := widget.NewToolbar( widget.NewToolbarAction(theme.DocumentCreateIcon(), func() { log.Println(\u0026#34;新建文档\u0026#34;) }), widget.NewToolbarSeparator(), widget.NewToolbarAction(theme.ContentCutIcon(), func() {}), widget.NewToolbarAction(theme.ContentCopyIcon(), func() {}), widget.NewToolbarAction(theme.ContentPasteIcon(), func() {}), widget.NewToolbarSpacer(), widget.NewToolbarAction(theme.HelpIcon(), func() { log.Println(\u0026#34;显示帮助\u0026#34;) }), ) content := container.NewBorder(toolbar, nil, nil, nil, widget.NewLabel(\u0026#34;内容\u0026#34;)) myWindow.SetContent(content) myWindow.ShowAndRun() } ","date":"2025-03-15","externalUrl":null,"permalink":"/docs/gofyne/05-widget/07-toolbar/","section":"教程","summary":"工具栏控件使用图标创建一行动作按钮来表示每个操作。widget.NewToolbar(...) 构造函数接受一系列 widget.ToolbarItem 参数。内置的工具栏项目类型有动作，分隔符和空格器。\n","title":"工具栏 Toolbar","type":"docs"},{"content":"layout.MaxLayout是最简单的布局，它将容器中的所有项目设置为与容器相同的大小。这通常在一般的容器中不太有用，但在组合控件时可能适用。\n最大布局会扩展容器，使其至少与最大项目的最小大小相同。对象将按照它们传递给容器的顺序被绘制，最后传递的对象将被绘制在最上面。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/layout\u0026#34; \u0026#34;fyne.io/fyne/v2/theme\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;Max Layout\u0026#34;) img := canvas.NewImageFromResource(theme.FyneLogo()) text := canvas.NewText(\u0026#34;Overlay\u0026#34;, color.Black) content := container.New(layout.NewMaxLayout(), img, text) myWindow.SetContent(content) myWindow.ShowAndRun() } ","date":"2025-02-16","externalUrl":null,"permalink":"/docs/gofyne/04-container/07-max/","section":"教程","summary":"layout.MaxLayout是最简单的布局，它将容器中的所有项目设置为与容器相同的大小。这通常在一般的容器中不太有用，但在组合控件时可能适用。\n","title":"最大布局 Max","type":"docs"},{"content":"最后一个画布原始类型是 Gradient，可用作 canvas.LinearGradient 和 canvas.RadialGradient，用于绘制从一种颜色到另一种颜色的渐变，有多种模式。你可以使用 NewHorizontalGradient()、NewVerticalGradient() 或 NewRadialGradient() 创建渐变。\n要创建一个渐变，你需要一个起始颜色和结束颜色——画布会计算其中的每一个颜色。在这个示例中，我们使用 color.Transparent 来展示如何使用渐变（或任何其他类型）通过透明度值在背后的内容上实现半透明效果。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; ) func main() { myApp := app.New() w := myApp.NewWindow(\u0026#34;渐变\u0026#34;) gradient := canvas.NewHorizontalGradient(color.White, color.Transparent) //gradient := canvas.NewRadialGradient(color.White, color.Transparent) w.SetContent(gradient) w.Resize(fyne.NewSize(100, 100)) w.ShowAndRun() } ","date":"2025-01-20","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/07-gradient/","section":"教程","summary":"最后一个画布原始类型是 Gradient，可用作 canvas.LinearGradient 和 canvas.RadialGradient，用于绘制从一种颜色到另一种颜色的渐变，有多种模式。你可以使用 NewHorizontalGradient()、NewVerticalGradient() 或 NewRadialGradient() 创建渐变。\n","title":"渐变 Gradient","type":"docs"},{"content":"快捷键是可以通过键盘组合键或上下文菜单触发的常见任务。快捷键，很像键盘事件，可以附加到一个聚焦的元素上，或者在Canvas上注册，以便在Window中始终可用。\n在Canvas中注册 # 有许多标准的快捷键定义（如fyne.ShortcutCopy），它们连接到标准键盘快捷键和右键菜单。添加新Shortcut的第一步是定义快捷键。对于大多数用途，这将是一个键盘触发的快捷键，这是一个桌面扩展。为此，我们使用desktop.CustomShortcut，例如，要使用Tab键和Control修饰符，你可能会做如下操作：\nctrlTab := \u0026amp;desktop.CustomShortcut{KeyName: fyne.KeyTab, Modifier: fyne.KeyModifierControl} ctrlAltTab := \u0026amp;desktop.CustomShortcut{KeyName: fyne.KeyTab, Modifier: fyne.KeyModifierControl | fyne.KeyModifierAlt} 注意，这个快捷键可以重复使用，所以你可以将它附加到菜单或其他项上。在这个示例中，我们希望它始终可用，所以我们将其注册到我们窗口的Canvas上，如下所示：\nctrlTab := \u0026amp;desktop.CustomShortcut{KeyName: fyne.KeyTab, Modifier: fyne.KeyModifierControl} w.Canvas().AddShortcut(ctrlTab, func(shortcut fyne.Shortcut) { log.Println(\u0026#34;我们按下了Ctrl+Tab\u0026#34;) }) w.Canvas().AddShortcut(ctrlAltTab, func(shortcut fyne.Shortcut) { log.Println(\u0026#34;我们按下了Ctrl+Alt+Tab\u0026#34;) }) 如你所见，在这种方式注册快捷键有两个部分 - 传递快捷键定义以及回调函数。如果用户输入键盘快捷键，那么函数将被调用并打印输出。\n快捷键只能与修饰键结合使用。为了响应没有修饰键的键盘输入，请使用canvas.OnTypedRune或canvas.OnTypedKey。\n在输入框中添加快捷键 # 当当前项目聚焦时，让快捷键仅适用也是有帮助的。这种方法可以用于任何可聚焦的控件，并通过扩展该控件并添加TypedShortcut处理器来管理。这和添加键处理器很像，不同之处在于传入的值将是一个fyne.Shortcut。\ntype myEntry struct { widget.Entry } func (m *myEntry) TypedShortcut(s fyne.Shortcut) { if _, ok := s.(*desktop.CustomShortcut); !ok { m.Entry.TypedShortcut(s) return } log.Println(\u0026#34;输入了快捷键:\u0026#34;, s) } 从上面的摘录中你可以看到如何实现一个TypedShortcut处理器。在这个函数内部，你应该检查快捷键是否为之前使用的自定义类型。如果快捷键是标准的，调用原始快捷键处理器（如果控件有一个）是个好主意。 完成这些检查后，你可以将快捷键与你正在处理的各种类型进行比较（如果有多个）。\n","date":"2024-12-15","externalUrl":null,"permalink":"/docs/gofyne/02-explore/07-shortcuts/","section":"教程","summary":"快捷键是可以通过键盘组合键或上下文菜单触发的常见任务。快捷键，很像键盘事件，可以附加到一个聚焦的元素上，或者在Canvas上注册，以便在Window中始终可用。\n","title":"快捷键","type":"docs"},{"content":"一个好的测试套件的一部分是能够快速编写测试并定期运行它们。Fyne的API旨在使应用程序测试变得简单。通过将组件逻辑与其渲染定义分离，我们可以在不实际显示它们的情况下加载应用程序，并完全测试其功能。\n示例 # 我们可以通过扩展我们的Hello World应用程序来演示单元测试，包括为用户输入他们的名字以便问候的空间。我们首先更新用户界面，使其包含两个元素：一个用于问候的Label和一个用于输入名字的Entry。我们使用container.NewVBox（一个垂直盒子容器）将它们一个接一个地显示。更新后的用户界面代码如下所示：\nfunc makeUI() (*widget.Label, *widget.Entry) { return widget.NewLabel(\u0026#34;Hello world!\u0026#34;), widget.NewEntry() } func main() { a := app.New() w := a.NewWindow(\u0026#34;Hello Person\u0026#34;) w.SetContent(container.NewVBox(makeUI())) w.ShowAndRun() } 为了测试这个输入行为，我们创建了一个新文件（以 _test.go 结尾，将其标记为测试），定义了一个TestGreeter函数。\npackage main import ( \u0026#34;testing\u0026#34; ) func TestGreeting(t *testing.T) { } 我们可以添加一个验证初始状态的初始测试，为此我们测试从makeUI返回的Label的Text字段，如果它不正确，则错误测试。将以下代码添加到测试方法中：\nout, in := makeUI() if out.Text != \u0026#34;Hello world!\u0026#34; { t.Error(\u0026#34;Incorrect initial greeting\u0026#34;) } 这个测试将通过 - 接下来我们添加到测试中以验证问候者。我们使用Fyne的fyne.io/fyne/v2/test包来帮助测试场景，调用test.Type来模拟用户输入。以下测试代码将检查当输入用户姓名时输出是否更新（也确保添加了导入）：\ntest.Type(in, \u0026#34;Andy\u0026#34;) if out.Text != \u0026#34;Hello Andy!\u0026#34; { t.Error(\u0026#34;Incorrect user greeting\u0026#34;) } 你可以使用go test .运行所有这些测试 - 就像任何其他测试一样。这样做，你现在会看到一个失败 - 因为我们没有添加问候逻辑。将makeUI函数更新为以下代码：\nfunc makeUI() (*widget.Label, *widget.Entry) { out := widget.NewLabel(\u0026#34;Hello world!\u0026#34;) in := widget.NewEntry() in.OnChanged = func(content string) { out.SetText(\u0026#34;Hello \u0026#34; + content + \u0026#34;!\u0026#34;) } return out, in } 这样做，你会看到测试现在通过了。你也可以运行完整的应用程序（使用go run .），并且当你在Entry字段中输入名字时，看到问候更新。还要注意，所有这些测试都是在不显示窗口或窃取你的鼠标的情况下运行的 - 这是Fyne单元测试设置的另一个好处。\n","date":"2024-11-03","externalUrl":null,"permalink":"/docs/gofyne/01-started/07-testing/","section":"教程","summary":"一个好的测试套件的一部分是能够快速编写测试并定期运行它们。Fyne的API旨在使应用程序测试变得简单。通过将组件逻辑与其渲染定义分离，我们可以在不实际显示它们的情况下加载应用程序，并完全测试其功能。\n","title":"测试 GUI 应用程序","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/07-strategy/","section":"教程","summary":"","title":"Strategy","type":"docs"},{"content":"Backtrader 的启动和运行过程主要依赖于三个核心组件：\n数据源：提供市场数据，用于回测或实时交易。 策略：定义交易逻辑（基于类继承实现）。 Cerebro：核心管理器，负责整合数据源、策略，并启动回测或实时交易。 数据源 # 数据源是回测和策略运行的基础，为策略提供价格数据（如开盘价、最高价、最低价、收盘价）及其他市场信息。\n支持的数据源 # 本地数据文件：\n支持多种 CSV 格式（如 Yahoo Finance 数据）。 支持从 Pandas DataFrame 加载数据。 在线数据提取：\n提供内置的 Yahoo Finance 在线数据提取功能。 实时数据源：\n支持 Interactive Brokers (IB)、Visual Chart 和 Oanda 等实时数据源。 平台支持通过时间框架（如日线、5分钟线）和压缩级别（如1天、5分钟）自定义数据，适配不同交易策略。\n数据源设置示例 # 加载 Yahoo Finance 格式的 CSV 数据 # 以下是一个基本的 CSV 数据加载示例：\nimport backtrader as bt import datetime datapath = \u0026#39;path/to/your/yahoo/data.csv\u0026#39; data = bt.feeds.YahooFinanceCSVData( dataname=datapath, reversed=True # 如果数据是从最新日期到最早日期排列，需要设置为 True ) 如果数据跨越时间范围较长，可限制按时间限制加载的数据：\ndata = bt.feeds.YahooFinanceCSVData( dataname=datapath, reversed=True, # 如果数据是从最新日期到最早日期排列，需要设置为 True fromdate=datetime.datetime(2014, 1, 1), # 数据起始日期 todate=datetime.datetime(2014, 12, 31), # 数据结束日期 timeframe=bt.TimeFrame.Days, # 时间框架设置为日线 compression=1, # 每 1 天作为一个数据单位 name=\u0026#39;Yahoo Data\u0026#39; # 数据源命名（可选） ) 从 Pandas DataFrame 加载数据 # 如果你的数据存储在 Pandas DataFrame 中，可以使用以下方式加载：\nimport pandas as pd import backtrader as bt df = pd.read_csv(\u0026#39;path/to/your/data.csv\u0026#39;, index_col=\u0026#39;Date\u0026#39;, parse_dates=True) data = bt.feeds.PandasData( dataname=df, fromdate=datetime.datetime(2020, 1, 1), todate=datetime.datetime(2020, 12, 31), timeframe=bt.TimeFrame.Days ) 设置多时间框架 # 如果你需要同时加载 5 分钟和日线数据，可以分别加载不同时间框架的数据源：\ndata_5min = bt.feeds.GenericCSVData( dataname=\u0026#39;path/to/5min_data.csv\u0026#39;, timeframe=bt.TimeFrame.Minutes, compression=5 ) data_daily = bt.feeds.GenericCSVData( dataname=\u0026#39;path/to/daily_data.csv\u0026#39;, timeframe=bt.TimeFrame.Days, compression=1 ) cerebro.adddata(data_5min, name=\u0026#39;5min\u0026#39;) cerebro.adddata(data_daily, name=\u0026#39;daily\u0026#39;) 策略（派生类） # 策略是交易逻辑的核心，定义了如何根据数据执行买卖操作。Backtrader 中的策略通过继承 bt.Strategy 类实现。\n策略的基本方法 # 策略类至少需要实现以下两个方法：\n__init__：用于定义指标和变量。 next：逐条处理数据，执行交易逻辑。 传递不同时间框架的数据源时，next 基于主数据源迭代（即第一个传给 Cerebro 的数据），主数据源应是时间框架最小的数据源。\n使用 ReplayData 时，next 会被多次调用，特别是数据跨时间框架时，Backtrader 会在每个数据条上执行 next，模拟数据发展。\n示例策略 # 以下是一个简单的策略示例，基于 20 日简单移动平均线（SMA）进行交易：\nclass MyStrategy(bt.Strategy): def __init__(self): # 定义简单移动平均线（SMA） self.sma = bt.indicators.SimpleMovingAverage(self.data.close, period=20) def next(self): # 如果收盘价高于 SMA，则买入 if self.data.close[0] \u0026gt; self.sma[0]: self.buy() # 如果收盘价低于 SMA，则卖出 elif self.data.close[0] \u0026lt; self.sma[0]: self.sell() 扩展功能 # 策略还可以覆盖以下方法，为回测和实时交易添加更多功能：\nstart：回测开始时调用，用于初始化资源。 stop：回测结束时调用，用于清理资源或总结。 notify_order：订单状态变化时调用。 notify_trade：交易状态变化时调用。 演示策略如下所示：\nclass MyStrategy(bt.Strategy): def __init__(self): self.sma = bt.indicators.SimpleMovingAverage(self.data.close, period=20) def start(self): print(\u0026#39;回测开始！\u0026#39;) def stop(self): print(\u0026#39;回测结束！\u0026#39;) def notify_order(self, order): if order.status in [order.Completed]: print(f\u0026#34;订单完成: {order.info}\u0026#34;) def next(self): if self.data.close[0] \u0026gt; self.sma[0]: self.buy() elif self.data.close[0] \u0026lt; self.sma[0]: self.sell() 其他操作 # 在 Backtrader 中，策略类还提供各种交易操作。\n买入 / 卖出 / 平仓（buy / sell / close）： 通过这些方法，策略可以向经纪人发送订单。平台也允许手动创建订单并传递给经纪人，但使用内建方法更简单高效。\nclose：立即关闭当前市场头寸。\ngetposition（或属性 position）：返回当前市场头寸，可查看持有的仓位情况。\nsetsizer / getsizer（或属性 sizer）：设置或获取底层的股份定量器（Sizer）。定量器负责计算每次交易的仓位大小，可根据需要选择不同定量器类型，如固定大小、与资本成比例或指数等。\n策略类本身是一个 Line 对象，支持配置参数。\nclass MyStrategy(bt.Strategy): params = ((\u0026#39;period\u0026#39;, 20),) def __init__(self): self.sma = btind.SimpleMovingAverage(self.data, period=self.params.period) ... 现在 SimpleMovingAverage 不再使用固定的 20，而是根据策略中定义的 period 参数动态设置，提高了策略的灵活性。\nCerebro # 数据源和策略就绪后，Cerebro 实例会将所有内容整合执行。创建 Cerebro 实例很简单：\ncerebro = bt.Cerebro() 没有特殊需求时，默认配置会自动处理以下内容：\n创建默认经纪人 操作不收佣金 数据源预加载 默认执行模式为 runonce（批处理模式），这是最快的方式 所有指标都需要支持 runonce 模式，以确保最佳执行速度。平台内置的指标大多支持此模式。\n典型流程 # 创建 Cerebro 实例\ncerebro = bt.Cerebro() 添加数据源\ncerebro.adddata(data) 添加策略\ncerebro.addstrategy(MyStrategy) 配置经纪人\n设置初始资金、佣金等。\ncerebro.broker.set_cash(100000) # 设置初始资金 cerebro.broker.setcommission(commission=0.001) # 设置佣金 运行回测\ncerebro.run() 绘制回测结果\ncerebro.plot() plot 接受一些自定义参数：\nnumfigs=1：如果绘图过于密集，可分成多个图 plotter=None：可传递自定义绘图器实例，Cerebro 将不使用默认绘图器 策略优化 # Backtrader 支持对策略参数进行优化。例如，测试不同 SMA 周期参数对策略的影响：\ncerebro.optstrategy(MyStrategy, period=range(10, 50, 5)) 上例中，period 从 10 到 45（每次递增 5）进行测试，平台自动运行每个参数组合的回测。\n完整代码示例 # 以下是一个完整的代码示例，整合了数据源、策略和 Cerebro：\nimport backtrader as bt from datetime import datetime # 定义策略 class MyStrategy(bt.Strategy): def __init__(self): self.sma = bt.indicators.SimpleMovingAverage(self.data.close, period=20) def next(self): if self.data.close[0] \u0026gt; self.sma[0]: self.buy() elif self.data.close[0] \u0026lt; self.sma[0]: self.sell() # 初始化 Cerebro cerebro = bt.Cerebro() # 加载数据 data = bt.feeds.YahooFinanceCSVData( dataname=\u0026#39;path/to/your/data.csv\u0026#39;, fromdate=datetime(2020, 1, 1), todate=datetime(2021, 1, 1) ) cerebro.adddata(data) # 添加策略 cerebro.addstrategy(MyStrategy) # 配置初始资金和佣金 cerebro.broker.setcash(100000) cerebro.broker.setcommission(commission=0.001) # 启动回测 cerebro.run() # 绘制结果 cerebro.plot() 通过这些核心组件的整合，Backtrader 提供了高度灵活的框架，适用于各种回测和实时交易需求。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/04-concepts/07-startup/","section":"教程","summary":"Backtrader 的启动和运行过程主要依赖于三个核心组件：\n数据源：提供市场数据，用于回测或实时交易。 策略：定义交易逻辑（基于类继承实现）。 Cerebro：核心管理器，负责整合数据源、策略，并启动回测或实时交易。 数据源 # 数据源是回测和策略运行的基础，为策略提供价格数据（如开盘价、最高价、最低价、收盘价）及其他市场信息。\n","title":"策略启动与运行流程","type":"docs"},{"content":" 了解如何入场（多头，self.buy）后，还需要了解如何退出以及策略是否持有头寸。\n本节演示入场后如何退出市场。逻辑很简单：入场持有 5 根 bar 后（在第 6 根 bar 上）退出，无论盈亏。同时简化逻辑：仅在未持仓且无进行中的订单时才允许买入。\n实现逻辑 # 要实现这个逻辑，需要确认订单成交时间、成交所在位置、当前是否有进行中的订单以及是否有持仓。\n订单状态 # 订单状态变化通过 notify_order 方法监听：\ndef notify_order(self, order): print(order.status) 订单状态包括 Submitted（已提交）、Accepted（已接受）、Completed（已成交）、Margin（保证金不足）、Rejected（已拒绝）。\n订单对象还包含其他信息，如执行价格 order.executed.price、已执行价值 order.executed.value、手续费 order.executed.comm。\n更多订单的信息，可查看 订单- Orders。\n成交位置 # 策略对象通过 len(self) 提供对当前 bar 位置的访问。next 方法没有直接提供 bar 索引，但 len(self) 告诉你当前线的长度。\n我们只需记下订单完成时的长度，然后检查当前长度是否已超过 5 根 bar。\ndef notify_order(self, order): if order.status in [order.Completed]: self.bar_executed = len(self) def next(self): # 其他代码 if len(self) \u0026gt;= (self.bar_executed + 5): # 卖出退场 此处无 \u0026ldquo;时间\u0026rdquo; 或 \u0026ldquo;时间框架\u0026rdquo; 含义，仅仅是 bar 的数量。bar可以代表1分钟、1小时、1天、1周或任何其他时间周期。尽管我们知道数据源是每日的，但策略不对其做任何假设。\n进行中的订单 # buy 和 sell 方法会返回创建的（尚未执行的）订单。我们可以记录创建的订单，待订单最终确认后（非 Submitted 或 Accepted 状态）将其清空。每次交易前检查是否有进行中的订单即可。\ndef notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return self.order = None def next(self): # 是否有进行中的订单 if self.order: return # 如果满足买入 self.order = self.buy() # 如果满足退出 self.order = self.sell() 持仓判断 # 通过 self.position 判断是否有持仓：\nif not self.position: # 入场逻辑 else: # 出场逻辑 完整示例 # from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) import backtrader as bt class TestStrategy(bt.Strategy): def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close self.order = None def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.log(\u0026#39;BUY EXECUTED, %.2f\u0026#39; % order.executed.price) elif order.issell(): self.log(\u0026#39;SELL EXECUTED, %.2f\u0026#39; % order.executed.price) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log(\u0026#39;Order Canceled/Margin/Rejected\u0026#39;) self.order = None def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) if self.order: return if not self.position: if self.dataclose[0] \u0026lt; self.dataclose[-1]: if self.dataclose[-1] \u0026lt; self.dataclose[-2]: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.buy() else: if len(self) \u0026gt;= (self.bar_executed + 5): self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.sell() if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() cerebro.addstrategy(TestStrategy) modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, \u0026#39;../../datas/orcl-1995-2014.txt\u0026#39;) data = bt.feeds.YahooFinanceCSVData( dataname=datapath, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), reverse=False) cerebro.adddata(data) cerebro.broker.setcash(100000.0) print(\u0026#39;Starting Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) cerebro.run() print(\u0026#39;Final Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) 输出：\nStarting Portfolio Value: 100000.00 2000-01-03, Close, 27.85 2000-01-04, Close, 25.39 2000-01-05, Close, 24.05 2000-01-05, BUY CREATE, 24.05 2000-01-06, BUY EXECUTED, 23.61 2000-01-06, Close, 22.63 2000-01-07, Close, 24.37 2000-01-10, Close, 27.29 2000-01-11, Close, 26.49 2000-01-12, Close, 24.90 2000-01-13, Close, 24.77 2000-01-13, SELL CREATE, 24.77 2000-01-14, SELL EXECUTED, 25.70 ... ... ... 2000-12-15, SELL CREATE, 26.93 2000-12-18, SELL EXECUTED, 28.29 2000-12-18, Close, 30.18 2000-12-19, Close, 28.88 2000-12-20, Close, 26.88 2000-12-20, BUY CREATE, 26.88 2000-12-21, BUY EXECUTED, 26.23 2000-12-21, Close, 27.82 2000-12-22, Close, 30.06 2000-12-26, Close, 29.17 2000-12-27, Close, 28.94 2000-12-28, Close, 29.29 2000-12-29, Close, 27.41 Final Portfolio Value: 100018.53 现在系统盈利了，我们又前进了一步。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/06-sell/","section":"教程","summary":" 了解如何入场（多头，self.buy）后，还需要了解如何退出以及策略是否持有头寸。\n本节演示入场后如何退出市场。逻辑很简单：入场持有 5 根 bar 后（在第 6 根 bar 上）退出，无论盈亏。同时简化逻辑：仅在未持仓且无进行中的订单时才允许买入。\n","title":"卖出操作与平仓机制","type":"docs"},{"content":"并非所有提供商都提供连续期货合约数据。有时提供的是仍在交易中的到期合约数据，这使得回测很不方便，因为数据分散在多个合约上，且合约在时间上重叠。\n将过去的合约数据连接成连续数据流可以缓解这个问题。但：\n没有唯一的最佳方法将不同到期日的数据连接成连续期货数据 可参考一些文献，如 SierraChart 的文章 滚动数据源 # 从 backtrader 1.8.10.99 开始，增加了将不同到期日的期货数据连接成连续期货的功能：\nimport backtrader as bt cerebro = bt.Cerebro() data0 = bt.feeds.MyFeed(dataname=\u0026#39;Expiry0\u0026#39;) data1 = bt.feeds.MyFeed(dataname=\u0026#39;Expiry1\u0026#39;) ... dataN = bt.feeds.MyFeed(dataname=\u0026#39;ExpiryN\u0026#39;) drollover = cerebro.rolloverdata(data0, data1, ..., dataN, name=\u0026#39;MyRoll\u0026#39;, **kwargs) cerebro.run() 注意：\n**kwargs 将在下文解释 也可以直接使用 RollOver 数据源（子类化时需要）： import backtrader as bt cerebro = bt.Cerebro() data0 = bt.feeds.MyFeed(dataname=\u0026#39;Expiry0\u0026#39;) data1 = bt.feeds.MyFeed(dataname=\u0026#39;Expiry1\u0026#39;) ... dataN = bt.feeds.MyFeed(dataname=\u0026#39;ExpiryN\u0026#39;) drollover = bt.feeds.RollOver(data0, data1, ..., dataN, dataname=\u0026#39;MyRoll\u0026#39;, **kwargs) cerebro.adddata(drollover) cerebro.run() 注意：\n使用 RollOver 时，通过 dataname 参数分配名称，这是所有数据源用于传递名称/代码的标准参数。这里被重用于给整个滚动期货集分配一个通用名称。 对于 cerebro.rolloverdata，使用 name 参数为该数据源分配名称。 Rollover 的使用概括如下：\n按通常方式创建数据源，但不要添加到 cerebro 将这些数据源作为输入传递给 bt.feeds.RollOver 同时传递一个 dataname 用于标识 然后将滚动数据源添加到 cerebro 滚动的选项 # 提供两个参数来控制滚动过程：\n参数名 默认值 描述 checkdate None 必须是一个可调用对象，签名：checkdate(dt, d)- dt 一个datetime.datetime对象 - d，当前活跃期货的数据源 预期返回值- True：只要可调用对象返回此值，就可以切换到下一个期货 - False：不能进行到期转换例如，如果某商品在3月的第三个星期五到期，checkdate可以在到期所在的一整周内返回True。 checkcondition None 仅当checkdate返回True时才会调用此参数。如果为None，则内部评估为True（执行滚动）。否则，它必须是一个可调用对象，签名是 checkcondition(d0, d1) - d0是当前活跃期货的数据源- d1是下一个到期的数据源预期返回值：- True：滚动到下一个期货- False：不能进行到期转换例如，可以通过checkcondition判断，如果d0的交易量小于d1，则进行到期转换。 子类化RollOver # 如果指定的可调用对象仍不够用，可以子类化 RollOver。需要子类化的方法有：\ndef _checkdate(self, dt, d) 它与上文同名参数的签名相匹配。预期返回值也相同。\ndef _checkcondition(self, d0, d1) 它与上文同名参数的签名相匹配。预期返回值也相同。\n示例用法 # 注意： 示例默认使用 cerebro.rolloverdata，可以通过 -no-cerebro 标志改用 RollOver 加 cerebro.adddata。\n示例代码可在 backtrader 源代码中找到。\n期货拼接\n首先让我们通过运行不带参数的示例来看一个纯粹的拼接示例：\n$ ./rollover.py 输出结果如下：\nLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest 0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0 0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0 ... 0176, FESX, 199FESXM4, 2014-06-20, Fri, 3315.0, 3324.0, 3307.0, 3322.0, 134777.0, 520978.0 0177, FESX, 199FESXU4, 2014-06-23, Mon, 3301.0, 3305.0, 3265.0, 3285.0, 730211.0, 3003692.0 ... 0241, FESX, 199FESXU4, 2014-09-19, Fri, 3287.0, 3308.0, 3286.0, 3294.0, 144692.0, 566249.0 0242, FESX, 199FESXZ4, 2014-09-22, Mon, 3248.0, 3263.0, 3231.0, 3240.0, 582077.0, 2976624.0 ... 0306, FESX, 199FESXZ4, 2014-12-19, Fri, 3196.0, 3202.0, 3131.0, 3132.0, 226415.0, 677924.0 0307, FESX, 199FESXH5, 2014-12-22, Mon, 3151.0, 3177.0, 3139.0, 3168.0, 547095.0, 2952769.0 ... 0366, FESX, 199FESXH5, 2015-03-20, Fri, 3680.0, 3698.0, 3672.0, 3695.0, 147632.0, 887205.0 0367, FESX, 199FESXM5, 2015-03-23, Mon, 3654.0, 3655.0, 3608.0, 3618.0, 802344.0, 3521988.0 ... 0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0 0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0 可以看到，当一个数据源结束时，下一个数据源接管。\n切换总是在周五和周一之间发生：示例中的期货合约总是在周五到期。\n期货滚动（无检查） # 运行带 --rollover 参数的示例：\n$ ./rollover.py --rollover --plot 输出结果类似：\nLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest 0001, FESX, 199FESXM4, 2013-09-26, Thu , 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0 0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0 ... 0176, FESX, 199FESXM4, 2014-06-20, Fri, 3315.0, 3324.0, 3307.0, 3322.0, 134777.0, 520978.0 0177, FESX, 199FESXU4, 2014-06-23, Mon, 3301.0, 3305.0, 3265.0, 3285.0, 730211.0, 3003692.0 ... 0241, FESX, 199FESXU4, 2014-09-19, Fri, 3287.0, 3308.0, 3286.0, 3294.0, 144692.0, 566249.0 0242, FESX, 199FESXZ4, 2014-09-22, Mon, 3248.0, 3263.0, 3231.0, 3240.0, 582077.0, 2976624.0 ... 0306, FESX, 199FESXZ4, 2014-12-19, Fri, 3196.0, 3202.0, 3131.0, 3132.0, 226415.0, 677924.0 0307, FESX, 199FESXH5, 2014-12-22, Mon, 3151.0, 3177.0, 3139.0, 3168.0, 547095.0, 2952769.0 ... 0366, FESX, 199FESXH5, 2015-03-20, Fri, 3680.0, 3698.0, 3672.0, 3695.0, 147632.0, 887205.0 0367, FESX, 199FESXM5, 2015-03-23, Mon, 3654.0, 3655.0, 3608.0, 3618.0, 802344.0, 3521988.0 ... 0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0 0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0 可以看到，合约在 3 月、6 月、9 月和 12 月的第三个星期五更换。\n但这基本是错误的。虽然 backtrader 无法知道，但 EuroStoxx 50 期货在到期月的第三个星期五中午 12:00 CET 停止交易。因此在到期月的第三个星期五仍有一个每日条时更换合约，已经太晚了。\n在周内更换 # 示例中实现了一个 checkdate 可调用对象，用于计算当前活跃合约的到期日期。\ncheckdate 在到期周到达时允许滚动（如果周一为银行假日，滚动可能发生在周二）。\n运行带有--rollover和--checkdate参数的示例：\n$ ./rollover.py --rollover --checkdate --plot 输出结果类似：\nLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest 0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0 0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0 ... 0171, FESX, 199FESXM4, 2014-06-13, Fri, 3283.0, 3292.0, 3253.0, 3276.0, 734907.0, 2715357.0 0172, FESX, 199FESXU4, 2014-06-16, Mon, 3261.0, 3275.0, 3252.0, 3262.0, 180608.0, 844486.0 ... 0236, FESX, 199FESXU4, 2014-09-12, Fri, 3245.0, 3247.0, 3220.0, 3232.0, 650314.0, 2726874.0 0237, FESX, 199FESXZ4, 2014-09-15, Mon, 3209.0, 3224.0, 3203.0, 3221.0, 153448.0, 983793.0 ... 0301, FESX, 199FESXZ4, 2014-12-12, Fri, 3127.0, 3143.0, 3038.0, 3042.0, 1409834.0, 2934179.0 0302, FESX, 199FESXH5, 2014-12-15, Mon, 3041.0, 3089.0, 2963.0, 2980.0, 329896.0, 904053.0 ... 0361, FESX, 199FESXH5, 2015-03-13, Fri, 3657.0, 3680.0, 3627.0, 3670.0, 867678.0, 3499116.0 0362, FESX, 199FESXM5, 2015-03-16, Mon, 3594.0, 3641.0, 3588.0, 3629.0, 250445.0, 1056099.0 ... 0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0 0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0 效果更好。滚动现在发生在到期月第三个星期五之前的周一。\n添加交易量条件 # 还可以通过结合日期和交易量条件进一步改善，仅在新合约交易量超过当前活跃合约时切换。\n运行带有--rollover、--checkdate和--checkcondition参数的示例：\n$ ./rollover.py --rollover --checkdate --checkcondition --plot 输出结果类似：\nLen, Name, RollName, Datetime, WeekDay, Open, High, Low, Close, Volume, OpenInterest 0001, FESX, 199FESXM4, 2013-09-26, Thu, 2829.0, 2843.0, 2829.0, 2843.0, 3.0, 1000.0 0002, FESX, 199FESXM4, 2013-09-27, Fri, 2842.0, 2842.0, 2832.0, 2841.0, 16.0, 1101.0 ... 0175, FESX, 199FESXM4, 2014-06-19, Thu, 3307.0, 3330.0, 3300.0, 3321.0, 717979.0, 759122.0 0176, FESX, 199FESXU4, 2014-06-20, Fri, 3309.0, 3318.0, 3290 .0, 3298.0, 711627.0, 2957641.0 ... 0240, FESX, 199FESXU4, 2014-09-18, Thu, 3249.0, 3275.0, 3243.0, 3270.0, 846600.0, 803202.0 0241, FESX, 199FESXZ4, 2014-09-19, Fri, 3273.0, 3293.0, 3250.0, 3252.0, 1042294.0, 3021305.0 ... 0305, FESX, 199FESXZ4, 2014-12-18, Thu, 3095.0, 3175.0, 3085.0, 3172.0, 1309574.0, 889112.0 0306, FESX, 199FESXH5, 2014-12-19, Fri, 3195.0, 3200.0, 3106.0, 3147.0, 1329040.0, 2964538.0 ... 0365, FESX, 199FESXH5, 2015-03-19, Thu, 3661.0, 3691.0, 3646.0, 3668.0, 1271122.0, 1054639.0 0366, FESX, 199FESXM5, 2015-03-20, Fri, 3607.0, 3664.0, 3595.0, 3646.0, 1182235.0, 3407004.0 ... 0426, FESX, 199FESXM5, 2015-06-18, Thu, 3398.0, 3540.0, 3373.0, 3465.0, 1173246.0, 811805.0 0427, FESX, 199FESXM5, 2015-06-19, Fri, 3443.0, 3499.0, 3440.0, 3488.0, 104096.0, 516792.0 效果更好。切换日期移至到期月第三个星期五之前的周四。\n结论 # backtrader 提供了一个灵活的机制来创建连续期货数据流。\n示例用法 # $ ./rollover.py --help 输出：\nusage: rollover.py [-h] [--no-cerebro] [--rollover] [--checkdate] [--checkcondition] [--plot [kwargs]] Sample for Roll Over of Futures optional arguments: -h, --help show this help message and exit --no-cerebro Use RollOver Directly (default: False) --rollover --checkdate Change during expiration week (default: False) --checkcondition Change when a given condition is met (default: False) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example: --plot style=\u0026#34;candle\u0026#34; (to plot candles) (default: None) 示例代码：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import bisect import calendar import datetime import backtrader as bt class TheStrategy(bt.Strategy): def start(self): header = [\u0026#39;Len\u0026#39;, \u0026#39;Name\u0026#39;, \u0026#39;RollName\u0026#39;, \u0026#39;Datetime\u0026#39;, \u0026#39;WeekDay\u0026#39;, \u0026#39;Open\u0026#39;, \u0026#39;High\u0026#39;, \u0026#39;Low\u0026#39;, \u0026#39;Close\u0026#39;, \u0026#39;Volume\u0026#39;, \u0026#39;OpenInterest\u0026#39;] print(\u0026#39;, \u0026#39;.join(header)) def next(self): txt = list() txt.append(\u0026#39;%04d\u0026#39; % len(self.data0)) txt.append(\u0026#39;{}\u0026#39;.format(self.data0._dataname)) # Internal knowledge ... current expiration in use is in _d txt.append(\u0026#39;{}\u0026#39;.format(self.data0._d._dataname)) txt.append(\u0026#39;{}\u0026#39;.format(self.data.datetime.date())) txt.append(\u0026#39;{}\u0026#39;.format(self.data.datetime.date().strftime(\u0026#39;%a\u0026#39;))) txt.append(\u0026#39;{}\u0026#39;.format(self.data.open[0])) txt.append(\u0026#39;{}\u0026#39;.format(self.data.high[0])) txt.append(\u0026#39;{}\u0026#39;.format(self.data.low[0])) txt.append(\u0026#39;{}\u0026#39;.format(self.data.close[0])) txt.append(\u0026#39;{}\u0026#39;.format(self.data.volume[0])) txt.append(\u0026#39;{}\u0026#39;.format(self.data.openinterest[0])) print(\u0026#39;, \u0026#39;.join(txt)) def checkdate(dt, d): # Check if the date is in the week where the 3rd friday of Mar/Jun/Sep/Dec # EuroStoxx50 expiry codes: MY # M -\u0026gt; H, M, U, Z (Mar, Jun, Sep, Dec) # Y -\u0026gt; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 -\u0026gt; year code. 5 -\u0026gt; 2015 MONTHS = dict(H=3, M=6, U=9, Z=12) M = MONTHS[d._dataname[-2]] centuria, year = divmod(dt.year, 10) decade = centuria * 10 YCode = int(d._dataname[-1]) Y = decade + YCode if Y \u0026lt; dt.year: # Example: year 2019 ... YCode is 0 for 2020 Y += 10 exp_day = 21 - (calendar.weekday(Y, M, 1) + 2) % 7 exp_dt = datetime.datetime(Y, M, exp_day) # Get the year, week numbers exp_year, exp_week, _ = exp_dt.isocalendar() dt_year, dt_week, _ = dt.isocalendar() # print(\u0026#39;dt {} vs {} exp_dt\u0026#39;.format(dt, exp_dt)) # print(\u0026#39;dt_week {} vs {} exp_week\u0026#39;.format(dt_week, exp_week)) # can switch if in same week return (dt_year, dt_week) == (exp_year, exp_week) def checkvolume(d0, d1): return d0.volume[0] \u0026lt; d1.volume[0] # Switch if volume from d0 \u0026lt; d1 def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() fcodes = [\u0026#39;199FESXM4\u0026#39;, \u0026#39;199FESXU4\u0026#39;, \u0026#39;199FESXZ4\u0026#39;, \u0026#39;199FESXH5\u0026#39;, \u0026#39;199FESXM5\u0026#39;] store = bt.stores.VChartFile() ffeeds = [store.getdata(dataname=x) for x in fcodes] rollkwargs = dict() if args.checkdate: rollkwargs[\u0026#39;checkdate\u0026#39;] = checkdate if args.checkcondition: rollkwargs[\u0026#39;checkcondition\u0026#39;] = checkvolume if not args.no_cerebro: if args.rollover: cerebro.rolloverdata(name=\u0026#39;FESX\u0026#39;, *ffeeds, **rollkwargs) else: cerebro.chaindata(name=\u0026#39;FESX\u0026#39;, *ffeeds) else: drollover = bt.feeds.RollOver(*ffeeds, dataname=\u0026#39;FESX\u0026#39;, **rollkwargs) cerebro.adddata(drollover) cerebro.addstrategy(TheStrategy) cerebro.run(stdstats=False) if args.plot: pkwargs = dict(style=\u0026#39;bar\u0026#39;) if args.plot is not True: # evals to True but is not True npkwargs = eval(\u0026#39;dict(\u0026#39; + args.plot + \u0026#39;)\u0026#39;) # args were passed pkwargs.update(npkwargs) cerebro.plot(**pkwargs) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=\u0026#39;Sample for Roll Over of Futures\u0026#39;) parser.add_argument(\u0026#39;--no-cerebro\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;Use RollOver Directly\u0026#39;) parser.add_argument(\u0026#39;--rollover\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;) parser.add_argument(\u0026#39;--checkdate\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;Change during expiration week\u0026#39;) parser.add_argument(\u0026#39;--checkcondition\u0026#39;, required=False, action=\u0026#39;store_true\u0026#39;, help=\u0026#39;Change when a given condition is met\u0026#39;) # Plot options parser.add_argument(\u0026#39;--plot\u0026#39;, \u0026#39;-p\u0026#39;, nargs=\u0026#39;?\u0026#39;, required=False, metavar=\u0026#39;kwargs\u0026#39;, const=True, help=(\u0026#39;Plot the read data applying any kwargs passed\\n\u0026#39; \u0026#39;\\n\u0026#39; \u0026#39;For example:\\n\u0026#39; \u0026#39;\\n\u0026#39; \u0026#39; --plot style=\u0026#34;candle\u0026#34; (to plot candles)\\n\u0026#39;)) if pargs is not None: return parser.parse_args(pargs) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/08-datafeed-rollover/","section":"教程","summary":"并非所有提供商都提供连续期货合约数据。有时提供的是仍在交易中的到期合约数据，这使得回测很不方便，因为数据分散在多个合约上，且合约在时间上重叠。\n","title":"数据滚动与窗口管理","type":"docs"},{"content":"","date":"2025-04-17","externalUrl":null,"permalink":"/docs/gofyne/08-extend/","section":"教程","summary":"","title":"扩展新类型","type":"docs"},{"content":"AppTabs 容器用于允许用户在不同的内容面板之间切换。标签页要么只有文本，要么是文本和图标。建议不要混合使用一些标签页有图标而另一些没有图标的情况。使用 container.NewAppTabs(...) 创建标签容器，并传递 container.TabItem 项（可以使用 container.NewTabItem(...) 创建）。\n可以通过设置标签的位置来配置标签容器，位置选项包括 container.TabLocationTop、container.TabLocationBottom、container.TabLocationLeading 和 container.TabLocationTrailing。默认位置是顶部。\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; //\u0026#34;fyne.io/fyne/v2/theme\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { myApp := app.New() myWindow := myApp.NewWindow(\u0026#34;TabContainer 控件\u0026#34;) tabs := container.NewAppTabs( container.NewTabItem(\u0026#34;标签 1\u0026#34;, widget.NewLabel(\u0026#34;你好\u0026#34;)), container.NewTabItem(\u0026#34;标签 2\u0026#34;, widget.NewLabel(\u0026#34;世界！\u0026#34;)), ) //tabs.Append(container.NewTabItemWithIcon(\u0026#34;首页\u0026#34;, theme.HomeIcon(), widget.NewLabel(\u0026#34;首页标签\u0026#34;))) tabs.SetTabLocation(container.TabLocationLeading) myWindow.SetContent(tabs) myWindow.ShowAndRun() } 在移动设备上加载时，可能会忽略标签位置。在纵向方向，前导或尾随位置将被更改为底部。在横向方向，顶部或底部位置将移至前导。\n","date":"2025-02-19","externalUrl":null,"permalink":"/docs/gofyne/04-container/08-apptabs/","section":"教程","summary":"AppTabs 容器用于允许用户在不同的内容面板之间切换。标签页要么只有文本，要么是文本和图标。建议不要混合使用一些标签页有图标而另一些没有图标的情况。使用 container.NewAppTabs(...) 创建标签容器，并传递 container.TabItem 项（可以使用 container.NewTabItem(...) 创建）。\n","title":"Tab 布局 AppTabs","type":"docs"},{"content":"Fyne 包含一个动画框架，允许您在一段时间内平滑地过渡画布属性从一个值到另一个值。动画可以包含任何代码，这意味着可以管理任何类型的对象属性，但是内置了尺寸、位置和颜色的动画。\n动画通常使用画布包的内置助手创建，例如 NewSizeAnimation，并在创建的动画上调用 Start()。您可以设置动画以重复或自动反转，如下所示。\n首先让我们看看逐渐改变 Rectangle 填充颜色的颜色动画。在以下代码示例中，我们将矩形设置为窗口的内容，如之前的代码示例所做的那样。最大的不同是我们在显示窗口之前启动的动画。使用 NewColorRGBAAnimation 创建动画，它将在指定的 red 状态到 blue 状态之间过渡颜色通道，并将在指定的持续时间（2秒）内完成。\npackage main import ( \u0026#34;image/color\u0026#34; \u0026#34;time\u0026#34; \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/canvas\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;Hello\u0026#34;) obj := canvas.NewRectangle(color.Black) obj.Resize(fyne.NewSize(50, 50)) w.SetContent(container.NewWithoutLayout(obj)) red := color.NRGBA{R:0xff, A:0xff} blue := color.NRGBA{B:0xff, A:0xff} canvas.NewColorRGBAAnimation(red, blue, time.Second*2, func(c color.Color) { obj.FillColor = c canvas.Refresh(obj) }).Start() w.Resize(fyne.NewSize(250, 50)) w.SetPadded(false) w.ShowAndRun() } 也可以同时对多个属性进行动画处理。如果您仔细观察，您会看到我们将矩形添加到了没有布局的容器中——这意味着我们可以手动移动或调整对象的大小。让我们添加一个新的位置动画，它将在窗口中移动 Rectangle，并自动反转。\nmove := canvas.NewPositionAnimation(fyne.NewPos(0, 0), fyne.NewPos(200, 0), time.Second, obj.Move) move.AutoReverse = true move.Start() 因为 CanvasObject 的 Move() 函数期望一个 fyne.Position 参数，位置动画回调也是如此，我们可以简单地传递方法名称而不是创建一个新函数。如果您在第一个动画下面添加上面的代码，您会看到对象在改变颜色的同时跨越窗口移动！\n","date":"2025-01-23","externalUrl":null,"permalink":"/docs/gofyne/03-canvas/08-animation/","section":"教程","summary":"Fyne 包含一个动画框架，允许您在一段时间内平滑地过渡画布属性从一个值到另一个值。动画可以包含任何代码，这意味着可以管理任何类型的对象属性，但是内置了尺寸、位置和颜色的动画。\n","title":"动画 Animation","type":"docs"},{"content":"为应用程序存储用户配置和值是应用开发者的常见任务，但在多个平台上实现它可能既乏味又耗时。为了简化这个过程，Fyne 提供了一个 API，用于以清晰且易于理解的方式在文件系统上存储值，同时为您处理复杂的部分。\n让我们从 API 的设置开始。它是 Preferences 接口的一部分，其中存在用于 Bool、Float、Int 和 String 值的存储和加载功能。它们每个都包含三个不同的函数，一个用于加载，一个带有回退值的加载，最后一个用于存储值。下面为 String 类型展示了这三个函数及其行为：\n// String 查找键的字符串值 String(key string) string // StringWithFallback 查找字符串值，并在未找到时返回给定的回退值 StringWithFallback(key, fallback string) string // SetString 为给定键保存一个字符串值 SetString(key string, value string) 这些函数可以通过创建的应用变量并调用其 Preferences() 方法来访问。请注意，创建带有唯一 ID 的应用是必要的（通常像反转的 url 那样）。这意味着应用程序需要使用 app.NewWithID() 来创建，以拥有自己存储值的位置。它大致可以像下面的示例那样使用：\na := app.NewWithID(\u0026#34;com.example.tutorial.preferences\u0026#34;) [...] a.Preferences().SetBool(\u0026#34;Boolean\u0026#34;, true) number := a.Preferences().IntWithFallback(\u0026#34;ApplicationLuckyNumber\u0026#34;, 21) expression := a.Preferences().String(\u0026#34;RegularExpression\u0026#34;) [...] 为了展示这一点，我们将构建一个简单的小应用，它总是在设定的时间后关闭。这个超时应该是用户可更改的，并在应用程序的下一次启动时应用。\n让我们首先创建一个名为 timeout 的变量，用于以 time.Duration 的形式存储时间。\nvar timeout time.Duration 然后，我们可以创建一个选择控件，让用户从几个预定义的字符串中选择超时，然后将超时乘以字符串所对应的秒数。最后，使用 \u0026quot;AppTimeout\u0026quot; 键将字符串值设置为选定的值。\ntimeoutSelector := widget.NewSelect([]string{\u0026#34;10秒\u0026#34;, \u0026#34;30秒\u0026#34;, \u0026#34;1分钟\u0026#34;}, func(selected string) { switch selected { case \u0026#34;10秒\u0026#34;: timeout = 10 * time.Second case \u0026#34;30秒\u0026#34;: timeout = 30 * time.Second case \u0026#34;1分钟\u0026#34;: timeout = time.Minute } a.Preferences().SetString(\u0026#34;AppTimeout\u0026#34;, selected) }) 现在，我们想要获取设置的值，如果不存在，我们想要有一个回退，将超时设置为最短的一个，以节省用户等待时的时间。这可以通过将 timeoutSelector 的选定值设置为加载的值或回退值（如果是这种情况）来完成。通过这种方式，选择控件中的代码将针对该特定值运行。\ntimeoutSelector.SetSelected(a.Preferences().StringWithFallback(\u0026#34;AppTimeout\u0026#34;, \u0026#34;10秒\u0026#34;)) 最后一部分将只是有一个函数，在一个单独的 goroutine 中启动，并在选定的超时后告诉应用退出。\ngo func() { time.Sleep(timeout) a.Quit() }() 最终，结果代码应该看起来像这样：\npackage main import ( \u0026#34;time\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { a := app.NewWithID(\u0026#34;com.example.tutorial.preferences\u0026#34;) w := a.NewWindow(\u0026#34;超时\u0026#34;) var timeout time.Duration timeoutSelector := widget.NewSelect([]string{\u0026#34;10秒\u0026#34;, \u0026#34;30秒\u0026#34;, \u0026#34;1分钟\u0026#34;}, func(selected string) { switch selected { case \u0026#34;10秒\u0026#34;: timeout = 10 * time.Second case \u0026#34;30秒\u0026#34;: timeout = 30 * time.Second case \u0026#34;1分钟\u0026#34;: timeout = time.Minute } a.Preferences().SetString(\u0026#34;AppTimeout\u0026#34;, selected) }) timeoutSelector.SetSelected(a.Preferences().StringWithFallback(\u0026#34;AppTimeout\u0026#34;, \u0026#34;10秒\u0026#34;)) go func() { time.Sleep(timeout) a.Quit() }() w.SetContent(timeoutSelector) w.ShowAndRun() } 这段代码演示了如何利用 Fyne 的 Preferences API 来存储和读取用户的偏好设置，例如应用的超时设置。通过这种方式，开发者可以在多个平台上轻松地实现配置的存储和加载，而不需要处理底层的文件系统操作。Fyne 为处理复杂的部分提供了一个简单的界面，使得开发跨平台应用程序变得更加容易和直接。\n","date":"2024-12-18","externalUrl":null,"permalink":"/docs/gofyne/02-explore/08-preferences/","section":"教程","summary":"为应用程序存储用户配置和值是应用开发者的常见任务，但在多个平台上实现它可能既乏味又耗时。为了简化这个过程，Fyne 提供了一个 API，用于以清晰且易于理解的方式在文件系统上存储值，同时为您处理复杂的部分。\n","title":"偏好设置","type":"docs"},{"content":"将图形应用程序打包以供分发可能会很复杂。图形应用程序通常有与它们关联的图标和元数据，以及集成到每个环境所需的特定格式。Windows可执行文件需要嵌入图标，macOS应用是捆绑包，在Linux中有各种应该安装的元数据文件。多麻烦啊！\n幸运的是，“fyne”应用有一个“package”命令可以自动处理这一切。只需指定目标操作系统和任何所需的元数据（如图标），就会生成适当的包。对于.icns或.ico图标转换将自动完成，所以只需提供.png文件 :)。你需要做的就是已经为目标平台构建了应用程序\u0026hellip;\ngo install fyne.io/fyne/v2/cmd/fyne@latest fyne package -os darwin -icon myapp.png 如果你使用的是Go的旧版本（\u0026lt;1.16），你应该使用go get安装fyne\ngo get fyne.io/fyne/v2/cmd/fyne fyne package -os darwin -icon myapp.png 将创建myapp.app，一个完整的捆绑结构，用于分发给macOS用户。然后你也可以为Linux和Windows版本构建\u0026hellip;\nfyne package -os linux -icon myapp.png fyne package -os windows -icon myapp.png 这些命令将创建：\nmyapp.tar.gz，包含从usr/local/开始的文件夹结构，Linux用户可以将其展开到他们的系统根目录。 myapp.exe（在第二次构建后，这是Windows包所需的）将嵌入图标和应用元数据。 如果你只想在你的电脑上安装桌面应用程序，那么你可以使用有用的install子命令。例如，要将你当前的应用程序系统范围内安装，你可以简单地执行以下操作：\nfyne install -icon myapp.png 所有这些命令也支持默认图标文件Icon.png，这样你就可以避免每次执行时键入参数。从Fyne 2.1开始，还有一个元数据文件，你可以为你的项目设置默认选项。\n当然，如果你愿意，你仍然可以使用标准的Go工具运行你的应用程序。\n","date":"2024-11-06","externalUrl":null,"permalink":"/docs/gofyne/01-started/08-packaging/","section":"教程","summary":"将图形应用程序打包以供分发可能会很复杂。图形应用程序通常有与它们关联的图标和元数据，以及集成到每个环境所需的特定格式。Windows可执行文件需要嵌入图标，macOS应用是捆绑包，在Linux中有各种应该安装的元数据文件。多麻烦啊！\n","title":"打包桌面应用","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/08-indicators/","section":"教程","summary":"","title":"Indicator","type":"docs"},{"content":" 我们已经知道如何使用 backtrader 买卖交易了。本节介绍如何监控每笔交易的成本、利润和佣金。由于佣金的存在，利润分为毛利润和净利润。\n设置佣金 # 让我们先设置一个合理佣金率 - 0.1% （买入和卖出都要收取的），一行代码即可。\n# 0.1% ... 除以 100 以去掉百分号 cerebro.broker.setcommission(commission=0.001) 订单的成本和佣金 # 订单的成本和佣金可从 notify_order 回调中获取，order.executed.comm 为已执行佣金，order.executed.value 为投入的成本。\nclass TestStrategy(bt.Strategy): def notify_order(self, order): if order.status in [order.Completed]: if order.isbuy(): self.log( \u0026#39;买入执行，价格：%.2f，成本：%.2f，佣金 %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) else: self.log(\u0026#39;卖出执行，价格：%.2f，成本：%.2f，佣金 %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) 交易利润计算 # 利润的计算使用交易记录更简单。与订单类似，可通过 notify_trade 获取交易记录。trade.pnl 为毛利润，trade.pnlcomm 为净利润。\nclass TestStrategy(bt.Strategy): def notify_trade(self, trade): if not trade.isclosed: return self.log(\u0026#39;利润记录，毛利润 %.2f，净利润 %.2f\u0026#39; % (trade.pnl, trade.pnlcomm)) 只有平仓交易才有利润，故通过 trade.isclosed 判断只在平仓时输出利润信息。\n完整示例 # import datetime import os.path import sys import backtrader as bt class TestStrategy(bt.Strategy): def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close self.order = None self.buyprice = None self.buycomm = None def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.log( \u0026#39;买入执行，价格：%.2f，成本：%.2f，佣金 %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: self.log(\u0026#39;卖出执行，价格：%.2f，成本：%.2f，佣金 %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log(\u0026#39;订单取消/保证金不足/拒绝\u0026#39;) self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log(\u0026#39;利润记录，毛利润 %.2f，净利润 %.2f\u0026#39; % (trade.pnl, trade.pnlcomm)) def next(self): self.log(\u0026#39;收盘价，%.2f\u0026#39; % self.dataclose[0]) if self.order: return if not self.position: if self.dataclose[0] \u0026lt; self.dataclose[-1]: if self.dataclose[-1] \u0026lt; self.dataclose[-2]: self.log(\u0026#39;创建买入订单，%.2f\u0026#39; % self.dataclose[0]) self.order = self.buy() else: if len(self) \u0026gt;= (self.bar_executed + 5): self.log(\u0026#39;创建卖出订单，%.2f\u0026#39; % self.dataclose[0]) self.order = self.sell() if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() cerebro.addstrategy(TestStrategy) modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, \u0026#39;./datas/orcl-1995-2014.txt\u0026#39;) data = bt.feeds.YahooFinanceCSVData( dataname=datapath, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), reverse=False) cerebro.adddata(data) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission=0.001) print(\u0026#39;初始投资组合价值：%.2f\u0026#39; % cerebro.broker.getvalue()) cerebro.run() print(\u0026#39;最终投资组合价值：%.2f\u0026#39; % cerebro.broker.getvalue()) 执行后的输出是：\n初始投资组合价值：100000.00 2000-01-03T00:00:00, 收盘价，27.85 2000-01-04T00:00:00, 收盘价，25.39 2000-01-05T00:00:00, 收盘价，24.05 2000-01-05T00:00:00, 创建买入订单，24.05 2000-01-06T00:00:00, 买入执行，价格：23.61，成本：23.61，佣金 0.02 2000-01-06T00:00:00, 收盘价，22.63 2000-01-07T00:00:00, 收盘价，24.37 2000-01-10T00:00:00, 收盘价，27.29 2000-01-11T00:00:00, 收盘价，26.49 2000-01-12T00:00:00, 收盘价，24.90 2000-01-13T00:00:00, 收盘价，24.77 2000-01-13T00:00:00, 创建卖出订单，24.77 2000-01-14T00:00:00, 卖出执行，价格：25.70，成本：25.70，佣金 0.03 2000-01-14T00:00:00, 利润记录，毛利润 2.09，净利润 2.04 2000-01-14T00:00:00, 收盘价，25.18 ... ... ... 2000-12-15T00:00:00, 创建卖出订单，26.93 2000-12-18T00:00:00, 卖出执行，价格：28.29，成本：28.29，佣金 0.03 2000-12-18T00:00:00, 利润记录，毛利润 -0.06，净利润 -0.12 2000-12-18T00:00:00, 收盘价，30.18 2000-12-19T00:00:00, 收盘价，28.88 2000-12-20T00:00:00, 收盘价，26.88 2000-12-20T00:00:00, 创建买入订单，26.88 2000-12-21T00:00:00, 买入执行，价格：26.23，成本：26.23，佣金 0.03 2000-12-21T00:00:00, 收盘价，27.82 2000-12-22T00:00:00, 收盘价，30.06 2000-12-26T00:00:00, 收盘价，29.17 2000-12-27T00:00:00, 收盘价，28.94 2000-12-28T00:00:00, 收盘价，29.29 2000-12-29T00:00:00, 收盘价，27.41 2000-12-29T00:00:00, 创建卖出订单，27.41 最终投资组合价值：100016.98 让我们将其中的 \u0026ldquo;利润记录\u0026rdquo; 的日志提取出来。\n2000-01-14T00:00:00, 利润记录，毛利润 2.09，净利润 2.04 2000-02-07T00:00:00, 利润记录，毛利润 3.68，净利润 3.63 2000-02-28T00:00:00, 利润记录，毛利润 4.48，净利润 4.42 2000-03-13T00:00:00, 利润记录，毛利润 3.48，净利润 3.41 2000-03-22T00:00:00, 利润记录，毛利润 -0.41，净利润 -0.49 2000-04-07T00:00:00, 利润记录，毛利润 2.45，净利润 2.37 2000-04-20T00:00:00, 利润记录，毛利润 -1.95，净利润 -2.02 2000-05-02T00:00:00, 利润记录，毛利润 5.46，净利润 5.39 2000-05-11T00:00:00, 利润记录，毛利润 -3.74，净利润 -3.81 2000-05-30T00:00:00, 利润记录，毛利润 -1.46，净利润 -1.53 2000-07-05T00:00:00, 利润记录，毛利润 -1.62，净利润 -1.69 2000-07-14T00:00:00, 利润记录，毛利润 2.08，净利润 2.01 2000-07-28T00:00:00, 利润记录，毛利润 0.14，净利润 0.07 2000-08-08T00:00:00, 利润记录，毛利润 4.36，净利润 4.29 2000-08-21T00:00:00, 利润记录，毛利润 1.03，净利润 0.95 2000-09-15T00:00:00, 利润记录，毛利润 -4.26，净利润 -4.34 2000-09-27T00:00:00, 利润记录，毛利润 1.29，净利润 1.22 2000-10-13T00:00:00, 利润记录，毛利润 -2.98，净利润 -3.04 2000-10-26T00:00:00, 利润记录，毛利润 3.01，净利润 2.95 2000-11-06T00:00:00, 利润记录，毛利润 -3.59，净利润 -3.65 2000-11-16T00:00:00, 利润记录，毛利润 1.28，净利润 1.23 2000-12-01T00:00:00, 利润记录，毛利润 2.59，净利润 2.54 2000-12-18T00:00:00, 利润记录，毛利润 -0.06，净利润 -0.12 将这些 \u0026ldquo;净利润\u0026rdquo; 相加，最终的数字是 15.83。但系统告诉我们最终投资组合价值是 100016.98。显然，15.83 并不等于 16.98。\n这并非错误，净利润 15.83 是已平仓交易的利润。最后一天还有一个未平仓头寸——虽然已发送卖出操作，还要等待下个 bar 才能成交。最终投资组合价值止于 2000-12-29 的收盘价。该订单的成交价格将在下一个交易日（2001-01-02）设定。\n假设我们将数据源扩展到下一个交易日：\n2001-01-02T00:00:00, 卖出执行，价格：27.87，成本：27.87，佣金 0.03 2001-01-02T00:00:00, 利润记录，毛利润 1.64，净利润 1.59 2001-01-02T00:00:00, 收盘价，24.87 2001-01-02T00:00:00, 创建买入订单，24.87 最终投资组合价值：100017.41 现在将先前的净利润与已完成操作的净利润相加：\n15.83 + 1.59 = 17.42 忽略四舍五入误差，现在就对上了。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/07-commission/","section":"教程","summary":" 我们已经知道如何使用 backtrader 买卖交易了。本节介绍如何监控每笔交易的成本、利润和佣金。由于佣金的存在，利润分为毛利润和净利润。\n","title":"交易监控与佣金设置","type":"docs"},{"content":"该功能是较晚加入到 Backtrader 中的，为适应已有的内部结构做了一些调整。因此它在灵活性和功能完备性上可能不如预期，但在许多情况下仍然够用。\n尽管实现时尝试支持即插即用的过滤器链，但受限于原有内部结构，无法始终保证。因此，有些过滤器可以链式使用，有些则不能。\n目的 # 将数据源提供的值转换为不同的数据流。\n该实现最初是为了简化两个明显的过滤器：重采样 和 重放，它们可通过 cerebro API 直接使用。\n重采样（cerebro.resampledata）：改变传入数据流的时间框架和压缩比例，如 (秒, 1) -\u0026gt; (天, 1)。重采样过滤器会拦截并缓冲数据，直到能够提供 1 天的条形数据（在遇到第二天的 1 秒数据时触发）。\n重放（cerebro.replaydata）：利用 1 秒分辨率的数据重建 1 天的条形数据。1 天的条形数据会被反复传递并更新，直到所有 1 秒数据都显示完毕，从而模拟实际交易日的发展。\n注意，在日期未变化时，数据的长度（len(data)）和策略的长度保持不变。\n工作原理 # 使用 addfilter 方法为已有数据源添加过滤器：\ndata = MyDataFeed(dataname=myname) data.addfilter(filter, *args, **kwargs) cerebro.adddata(data) 即使与重采样或重放过滤器一起使用，也可以这样做：\ndata = MyDataFeed(dataname=myname) data.addfilter(filter, *args, **kwargs) cerebro.replaydata(data) 过滤器接口 # 过滤器必须符合以下接口要求。首先，要是一个可调用的对象，接受如下签名：\ncallable(data, *args, **kwargs) 或一个可以实例化并被调用的类，在实例化时其__init__方法必须支持以下签名：\ndef __init__(self, data, *args, **kwargs) __call__方法的签名为：\ndef __call__(self, data, *args, **kwargs) 每当新的数据流值到来时，实例都会被调用。*args 和 **kwargs 与 __init__ 中传递的参数相同。\n返回值 描述 True 数据流的内部获取循环需要重新尝试从数据源获取数据，因为长度被修改。 False 即使数据已被修改（如修改了 close 价格），数据流的长度不变。 基于类的过滤器还可以实现一个额外方法 last，签名如下：\ndef last(self, data, *args, **kwargs) 当数据流结束时调用，允许过滤器推送缓冲的数据。例如重采样时，一个条形数据会被缓冲直到看到下一个时间段的数据。如果数据流结束，last 方法提供了推送缓冲数据的机会。\n注意\n如果过滤器没有参数，且添加时也没有额外参数，签名可简化为：\ndef __init__(self, data): ... 示例过滤器 # class SessionFilter(object): def __init__(self, data): pass def __call__(self, data): if data.p.sessionstart \u0026lt;= data.datetime.time() \u0026lt;= data.p.sessionend: # 在交易时段内 return False # 告诉外部数据循环，当前条形数据可以继续处理 # 在常规交易时段外 data.backwards() # 从数据堆栈中移除该条形数据 return True # 告诉外部数据循环，必须获取新的条形数据 该过滤器：\n使用 data.p.sessionstart 和 data.p.sessionend 判断 Bar 是否在交易时段。 在交易时段内返回 False，表示当前条形数据可以继续处理。 不在交易时段时移除条形数据并返回 True，表示需要获取新数据。 注意，data.backwards() 使用了 LineBuffer 接口，涉及 backtrader 的内部实现。\n使用场景 # 有些数据源包含非交易时段的数据，这些数据可能对交易者没有意义。使用此过滤器后，只有交易时段内的条形数据才会被考虑。\n过滤器的数据伪 API\n上面的示例展示了如何通过 data.backwards() 从数据流中移除当前条形数据。数据源对象还提供了一些有用的伪 API 调用：\ndata.backwards(size=1, force=False)：从数据流中移除 size 条数据（默认 1），通过移动逻辑指针实现。如果 force=True，同时移除物理存储。 data.forward(value=float('NaN'), size=1)：在数据流前面添加 size 条数据，必要时增加物理存储，用 value 填充。 data._addtostack(bar, stash=False)：将条形数据 bar 添加到堆栈。如果 stash=False，下一轮迭代立即处理；如果 stash=True，则经过完整的处理循环（包括可能被过滤器重新解析）。 data._save2stack(erase=False, force=False)：将当前条形数据保存到堆栈供稍后处理。如果 erase=True，调用 data.backwards() 并传递 force 参数。 data._updatebar(bar, forward=False, ago=0)：用 bar 中的值覆盖数据流中相应位置。ago=0 更新当前条形数据，ago=-1 更新前一个条形数据。 另一个示例：Pinkfish过滤器 # 这是一个可以链式使用的过滤器，特别是与重放过滤器一起使用。Pinkfish 的概念是通过每日数据执行通常需要即时数据才能完成的操作。\n实现方法：\n将每日条形数据分成两个部分：OHL 和 C。\n这些部分与重放过滤器串联后，数据流呈现如下形式：\nWith Len X -\u0026gt; OHL With Len X -\u0026gt; OHLC With Len X + 1 -\u0026gt; OHL With Len X + 1 -\u0026gt; OHLC With Len X + 2 -\u0026gt; OHL With Len X + 2 -\u0026gt; OHLC ... 逻辑：\n接收到 OHLC 条形数据时，复制并拆解为 OHL 和 C 两个部分。 OHL 条形数据的收盘价被替换为开盘、最高、最低价的平均值。 C 条形数据即 “tick”，收盘价用于填充四个价格字段。 OHL 部分立即加入堆栈，C 部分推迟处理。 该过滤器与重放过滤器配合工作，合并 OHL 和 CCCC 部分，最终输出 OHLC 条形数据。\n使用场景 # 例如，当今天最大值是过去20个交易日中的最高值时，可发出“关闭”订单，并在第二次tick时执行。\nclass DaySplitter_Close(bt.with_metaclass(bt.MetaParams, object)): \u0026#39;\u0026#39;\u0026#39; Splits a daily bar in two parts simulating 2 ticks which will be used to replay the data: - First tick: ``OHLX`` The ``Close`` will be replaced by the *average* of ``Open``, ``High`` and ``Low`` The session opening time is used for this tick and - Second tick: ``CCCC`` The ``Close`` price will be used for the four components of the price The session closing time is used for this tick The volume will be split amongst the 2 ticks using the parameters: - ``closevol`` (default: ``0.5``) The value indicate which percentage, in absolute terms from 0.0 to 1.0, has to be assigned to the *closing* tick. The rest will be assigned to the ``OHLX`` tick. **This filter is meant to be used together with** ``cerebro.replaydata`` \u0026#39;\u0026#39;\u0026#39; params = ( (\u0026#39;closevol\u0026#39;, 0.5), # 0 -\u0026gt; 1 amount of volume to keep for close ) # replaying = True def __init__(self, data): self.lastdt = None def __call__(self, data): # Make a copy of the new bar and remove it from stream datadt = data.datetime.date() # keep the date if self.lastdt == datadt: return False # skip bars that come again in the filter self.lastdt = datadt # keep ref to last seen bar # Make a copy of current data for ohlbar ohlbar = [data.lines[i][0] for i in range(data.size())] closebar = ohlbar[:] # Make a copy for the close # replace close price with o-h-l average ohlprice = ohlbar[data.Open] + ohlbar[data.High] + ohlbar[data.Low] ohlbar[data.Close] = ohlprice / 3.0 vol = ohlbar[data.Volume] # adjust volume ohlbar[data.Volume] = vohl = int(vol * (1.0 - self.p.closevol)) oi = ohlbar[data.OpenInterest] # adjust open interst ohlbar[data.OpenInterest] = 0 # Adjust times dt = datetime.datetime.combine(datadt, data.p.sessionstart) ohlbar[data.DateTime] = data.date2num(dt) # Ajust closebar to generate a single tick -\u0026gt; close price closebar[data.Open] = cprice = closebar[data.Close] closebar[data.High] = cprice closebar[data.Low] = cprice closebar[data.Volume] = vol - vohl ohlbar[data.OpenInterest] = oi # Adjust times dt = datetime.datetime.combine(datadt, data.p.sessionend) closebar[data.DateTime] = data.date2num(dt) # Update stream data.backwards(force=True) # remove the copied bar from stream data._add2stack(ohlbar) # add ohlbar to stack # Add 2nd part to stash to delay processing to next round data._add2stack(closebar, stash=True) return False # initial tick can be further processed from stack ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/09-datafeed-filters/","section":"教程","summary":"该功能是较晚加入到 Backtrader 中的，为适应已有的内部结构做了一些调整。因此它在灵活性和功能完备性上可能不如预期，但在许多情况下仍然够用。\n尽管实现时尝试支持即插即用的过滤器链，但受限于原有内部结构，无法始终保证。因此，有些过滤器可以链式使用，有些则不能。\n","title":"数据过滤器 Filter 详解","type":"docs"},{"content":"","date":"2025-05-08","externalUrl":null,"permalink":"/docs/gofyne/09-architecture/","section":"教程","summary":"","title":"架构","type":"docs"},{"content":"自 v2.2.0 版本以来，Fyne 内置了对系统托盘菜单的支持。此功能在 macOS、Windows 和 Linux 计算机上显示一个图标，当点击时，会弹出由应用程序指定的菜单。\n由于这是一个特定于桌面的功能，我们首先必须进行运行时检查，以确认应用程序正在桌面模式下运行。为此，我们进行 Go 类型断言以获取对桌面功能的引用：\nif desk, ok := a.(desktop.App); ok { ... } 如果 ok 变量为真，则我们可以使用标准 Fyne 菜单 API 设置菜单，你可能之前在 Window.SetMainMenu 中使用过这个 API。\nm := fyne.NewMenu(\u0026#34;MyApp\u0026#34;, fyne.NewMenuItem(\u0026#34;Show\u0026#34;, func() { log.Println(\u0026#34;点击了显示\u0026#34;) })) desk.SetSystemTrayMenu(m) 将此代码添加到应用程序的设置中，运行应用程序，你会看到系统托盘中显示了一个 Fyne 图标。当你点击它时，会出现一个包含“显示”和“退出”的菜单。\n默认图标是 Fyne 标志，你可以通过应用元数据更改这个设置，或者通过 App.SetIcon 或直接使用 desk.SetSystemTrayIcon 为系统托盘设置应用图标。\n管理窗口生命周期 # 默认情况下，当你关闭所有窗口时 Fyne 应用将会退出，这可能不是你希望的系统托盘应用的行为。要覆盖此行为，你可以使用 Window.SetCloseIntercept 功能来覆盖窗口关闭时的操作。在下面的示例中，我们通过调用 Window.Hide() 来隐藏窗口而不是关闭它。在第一次显示窗口之前添加这个。\nw.SetCloseIntercept(func() { w.Hide() }) 隐藏窗口的好处是你可以简单地再次使用 Window.Show() 显示它，如果需要第二次使用相同的内容，这比创建一个新窗口更高效。我们更新之前创建的菜单以显示上面隐藏的窗口。\nfyne.NewMenuItem(\u0026#34;Show\u0026#34;, func() { w.Show() })) 完整的应用程序 # 这就是使用 Fyne 设置系统托盘菜单的全部内容！本教程的完整代码如下所示。\npackage main import ( \u0026#34;fyne.io/fyne/v2\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/driver/desktop\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;SysTray\u0026#34;) if desk, ok := a.(desktop.App); ok { m := fyne.NewMenu(\u0026#34;MyApp\u0026#34;, fyne.NewMenuItem(\u0026#34;Show\u0026#34;, func() { w.Show() })) desk.SetSystemTrayMenu(m) } w.SetContent(widget.NewLabel(\u0026#34;Fyne 系统托盘\u0026#34;)) w.SetCloseIntercept(func() { w.Hide() }) w.ShowAndRun() } ","date":"2024-12-21","externalUrl":null,"permalink":"/docs/gofyne/02-explore/09-systray/","section":"教程","summary":"自 v2.2.0 版本以来，Fyne 内置了对系统托盘菜单的支持。此功能在 macOS、Windows 和 Linux 计算机上显示一个图标，当点击时，会弹出由应用程序指定的菜单。\n","title":"系统托盘菜单","type":"docs"},{"content":"你的Fyne应用代码可以直接作为移动应用运行，就像它在桌面上做的那样。然而，将代码打包用于分发就复杂一些。这个页面将探索正是为了做到这一点而将你的应用带到iOS和Android上的过程。\n首先，你需要为移动打包安装更多的开发工具。对于Android构建，你必须安装Android SDK和NDK，并设置适当的环境，以便工具（如adb）可以在命令行中找到。要构建iOS应用，你需要在你的macOS电脑上安装Xcode以及命令行工具可选包。\n一旦你有了一个工作的开发环境，打包就很简单了。要为Android和iOS构建应用程序，以下命令将为你完成所有操作。确保有一个唯一的应用程序标识符，因为在你首次发布后更改这些是不明智的。\nfyne package -os android -appID com.example.myapp -icon mobileIcon.png fyne package -os ios -appID com.example.myapp -icon mobileIcon.png 在这些命令完成后（首次编译可能需要一些时间），你将在你的目录中看到两个新文件，myapp.apk和myapp.app。你会看到后者与darwin应用程序捆绑包同名 - 不要将它们混淆，因为它们在另一个平台上不会工作。\n要在你的手机或模拟器上安装Android应用，只需调用：\nadb install myapp.apk 对于iOS，要在设备上安装，打开Xcode并在“Window”菜单中选择“Devices and Simulators”菜单项。然后找到你的手机并将myapp.app图标拖到你的应用列表上。\n如果你想在模拟器上安装，请确保使用iossimulator而不是ios打包你的应用程序\nfyne package -os iossimulator -appID com.example.myapp -icon mobileIcon.png 之后，你可以如下使用命令行工具：\nxcrun simctl install booted myapp.app 这些步骤介绍了如何为iOS和Android设备打包和安装Fyne应用程序，从而使Fyne成为开发跨平台移动应用的强大工具。\n","date":"2024-11-09","externalUrl":null,"permalink":"/docs/gofyne/01-started/09-mobile/","section":"教程","summary":"你的Fyne应用代码可以直接作为移动应用运行，就像它在桌面上做的那样。然而，将代码打包用于分发就复杂一些。这个页面将探索正是为了做到这一点而将你的应用带到iOS和Android上的过程。\n","title":"打包移动应用","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/09-orders/","section":"教程","summary":"","title":"Order","type":"docs"},{"content":" 前面的案例中，参数都硬编码在策略中。本节介绍如何在 backtrader 中自定义参数。\n定义参数 # 在策略中通过 params 属性定义参数，例如：\nclass TestStrategy: params = ((\u0026#39;myparam\u0026#39;, 27), (\u0026#39;exitbars\u0026#39;, 5),) 参数 myparam 的默认值是 27，exitbars 的默认值是 5。\n配置参数 # 我们可以在添加策略时修改参数默认值。\n# 添加策略时覆盖默认参数 cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7) 使用参数 # 策略代码中通过 self.params.param_name 访问参数。\n如下代码，通过参数 exitbars 修改退出逻辑：\nif len(self) \u0026gt;= (self.bar_executed + self.params.exitbars): 完整示例 # import backtrader as bt class TestStrategy(bt.Strategy): params = ( (\u0026#39;exitbars\u0026#39;, 5), ) def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close self.order = None self.buyprice = None self.buycomm = None def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.log(\u0026#39;BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: self.log(\u0026#39;SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log(\u0026#39;Order Canceled/Margin/Rejected\u0026#39;) self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log(\u0026#39;OPERATION PROFIT, GROSS %.2f, NET %.2f\u0026#39; % (trade.pnl, trade.pnlcomm)) def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) if self.order: return if not self.position: if self.dataclose[0] \u0026lt; self.dataclose[-1]: if self.dataclose[-1] \u0026lt; self.dataclose[-2]: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.buy() else: if len(self) \u0026gt;= (self.bar_executed + self.params.exitbars): self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.sell() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/08-parameters/","section":"教程","summary":" 前面的案例中，参数都硬编码在策略中。本节介绍如何在 backtrader 中自定义参数。\n定义参数 # 在策略中通过 params 属性定义参数，例如：\n","title":"策略参数的定义与使用","type":"docs"},{"content":" SessionFilter # class backtrader.filters.SessionFilter(data)\n过滤掉常规交易时间外的日内数据（即盘前/盘后数据）。\n这是一个”非简单”过滤器，必须管理数据栈。 不需要 “last” 方法，因为没有需要推送的数据。 SessionFilterSimple # class backtrader.filters.SessionFilterSimple(data)\n过滤掉常规交易时间外的日内数据（即盘前/盘后数据）。\n这是一个”简单”过滤器，不需要管理数据栈。 不需要 “last” 方法。 Bar 管理由 SimpleFilterWrapper 类处理，在 DataBase.addfilter_simple 调用期间添加。 SessionFiller # class backtrader.filters.SessionFiller(data)\n为声明的会话开始/结束时间内的数据源填充 Bar。\n参数：\nfill_price (默认: None): 为 None 时使用前一个 Bar 的收盘价。使用 float('NaN') 可生成不显示在图表中的 Bar。 fill_vol (默认: float(\u0026lsquo;NaN\u0026rsquo;)): 用于填充缺失交易量的值。 fill_oi (默认: float(\u0026lsquo;NaN\u0026rsquo;)): 用于填充缺失未平仓合约的值。 skip_first_fill (默认: True): 看到第一个有效 Bar 时，不从会话开始填充到此 Bar。 CalendarDays # class backtrader.filters.CalendarDays(data)\n填充缺失的日历日到交易日。\n参数：\nfill_price (默认: None): 0: 用给定值填充。 None: 使用上一个已知收盘价。 -1: 使用上一个 Bar 的中点（高/低平均值）。 fill_vol (默认: float(\u0026lsquo;NaN\u0026rsquo;)): 用于填充缺失交易量的值。 fill_oi (默认: float(\u0026lsquo;NaN\u0026rsquo;)): 用于填充缺失未平仓合约的值。 BarReplayer_Open # class backtrader.filters.BarReplayer_Open(data)\n将一个 Bar 分为两部分：\nOpen: 开盘价用于交付一个初始价格条，四个组件（OHLC）相等。 初始条的交易量/未平仓合约为 0。 OHLC: 原始条完整交付，包含原始交易量/未平仓合约。 分割模拟重播，无需使用重播过滤器。\nDaySplitter_Close # class backtrader.filters.DaySplitter_Close(data)\n将一个每日 Bar 分为两部分，模拟两个价格点以重播数据：\n第一个价格点: OHLX 收盘价替换为开盘价、最高价、最低价的平均值。 使用会话的开盘时间。 第二个价格点: CCCC 收盘价用于四个价格组件。 使用会话的收盘时间。 交易量在两个价格点之间分配： closevol (默认: 0.5): 分配给收盘价格点的比例（0.0 到 1.0），其余分配给 OHLX 价格点。 此过滤器与 cerebro.replaydata 配合使用。\nHeikinAshi # class backtrader.filters.HeikinAshi(data)\n重新建模开盘价、最高价、最低价、收盘价以形成 HeikinAshi 蜡烛图。\n参考：\nHeikin Ashi Candlesticks StockCharts Heikin Ashi Renko # class backtrader.filters.Renko(data)\n修改数据流以绘制 Renko 砖。\n参数：\nhilo (默认: False): 使用最高价和最低价代替收盘价来决定是否需要新砖。 size (默认: None): 每个砖块的大小。 autosize (默认: 20.0): 如果 size 为 None，用此值自动计算砖块大小（当前价格除以该值）。 dynamic (默认: False): 如果为 True 且使用 autosize，移动到新砖块时重新计算砖块大小。这会破坏 Renko 砖的完美对齐。 align (默认: 1.0): 砖块价格边界的对齐因子。例如，价格为 3563.25、align 为 10.0 时，对齐后的价格为 3560： 3563.25 / 10.0 = 356.325 四舍五入取整 -\u0026gt; 356 356 * 10.0 -\u0026gt; 3560 参考 # StockCharts Renko ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/10-datafeed-filters-referrence/","section":"教程","summary":"SessionFilter # class backtrader.filters.SessionFilter(data)\n过滤掉常规交易时间外的日内数据（即盘前/盘后数据）。\n这是一个”非简单”过滤器，必须管理数据栈。 不需要 “last” 方法，因为没有需要推送的数据。 SessionFilterSimple # class backtrader.filters.SessionFilterSimple(data)\n","title":"过滤器参数参考文档","type":"docs"},{"content":"","date":"2025-05-23","externalUrl":null,"permalink":"/docs/gofyne/10-faq/","section":"教程","summary":"","title":"常见问题","type":"docs"},{"content":"数据绑定在 Fyne v2.0.0 中引入，使得将许多控件连接到随时间更新的数据源变得更加容易。data/binding 包提供了许多有用的绑定，可以管理应用中使用的大多数标准类型。数据绑定可以使用绑定 API 管理（例如 NewString），也可以连接到外部数据项（如 BindInt(*int)）。\n支持绑定的控件通常有一个 ...WithData 构造器，在创建控件时设置绑定。你也可以调用 Bind() 和 Unbind() 来管理现有控件的数据。以下示例展示了如何管理一个与简单的 Label 控件绑定的 String 数据项。\npackage main import ( \u0026#34;time\u0026#34; \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/data/binding\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;Hello\u0026#34;) str := binding.NewString() go func() { dots := \u0026#34;.....\u0026#34; for i := 5; i \u0026gt;= 0; i-- { str.Set(\u0026#34;倒计时\u0026#34; + dots[:i]) time.Sleep(time.Second) } str.Set(\u0026#34;发射！\u0026#34;) }() w.SetContent(widget.NewLabelWithData(str)) w.ShowAndRun() } 你可以在本站的数据绑定部分了解更多信息。\n","date":"2024-12-24","externalUrl":null,"permalink":"/docs/gofyne/02-explore/10-binding/","section":"教程","summary":"数据绑定在 Fyne v2.0.0 中引入，使得将许多控件连接到随时间更新的数据源变得更加容易。data/binding 包提供了许多有用的绑定，可以管理应用中使用的大多数标准类型。数据绑定可以使用绑定 API 管理（例如 NewString），也可以连接到外部数据项（如 BindInt(*int)）。\n","title":"数据绑定","type":"docs"},{"content":"Fyne应用程序也可以通过标准的网络浏览器在网络上运行！由于不同引擎的标准和优势各异，这稍微复杂一些。\n使用Fyne创建的网络应用将提供一个WASM运行时以及一个JavaScript代码包，这使得生成的网页可以查看当前浏览器并选择适合其优势的实现方式。这在大多数系统上提供了极好的用户体验。\n为了准备你的应用通过网络使用，我们再次使用“fyne”命令行应用，它有一个用于快速测试的“serve”命令\ngo install fyne.io/fyne/v2/cmd/fyne@latest fyne serve 你会看到，过了一会儿，一个网页服务器已经在8080端口启动。只需在你的网络浏览器中打开https://localhost:8080，你就可以使用你的应用了！\n这个过程中使用的工具对Go版本非常敏感，因此可能会出现与版本不匹配相关的错误。如果你使用的是1.18版之后的Go，你应该确保你有1.18版的副本，并在你的环境中引用它，比如（对于一个基于homebrew的安装）：\nexport GOPHERJS_GOROOT=/opt/homebrew/Cellar/go@1.18/1.18.10/libexec 你可以在GopherJS文档上了解更多相关信息。\n打包用于网络分发 # fyne serve命令非常适合本地测试，但就像其他平台一样，你也会想要能够分发你的应用。为了准备上传的文件，就像常规的打包一样，使用fyne package命令。\nfyne package -os web 你也可以选择只为WASM或JavaScript打包，而不是自动检测设置：\nfyne package -os wasm fyne package -os js 演示 # 你可以访问demo.fyne.io，在任何设备上测试Fyne应用的实际运行情况。\n限制 # 截至v2.4.0版本发布，网络驱动程序还没有完全完成，所以你的应用可能无法使用以下功能：\n多窗口（但对话框都可以在当前窗口内部工作） 文档和偏好设置的存储 这些问题正在被解决，并将在未来的版本中得到解决。\n","date":"2024-11-12","externalUrl":null,"permalink":"/docs/gofyne/01-started/10-webapp/","section":"教程","summary":"Fyne应用程序也可以通过标准的网络浏览器在网络上运行！由于不同引擎的标准和优势各异，这稍微复杂一些。\n使用Fyne创建的网络应用将提供一个WASM运行时以及一个JavaScript代码包，这使得生成的网页可以查看当前浏览器并选择适合其优势的实现方式。这在大多数系统上提供了极好的用户体验。\n","title":"在浏览器中运行","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/10-broker/","section":"教程","summary":"","title":"Broker","type":"docs"},{"content":"2017 年 5 月，Yahoo 停用了原有的 CSV 格式历史数据下载 API。随后新的 API（这里称为 v7）被标准化并已实现，也带来了 CSV 下载格式的变化。\n使用 v7 API/格式 # 从 1.9.49.116 版本开始，这是默认行为。直接使用：\nYahooFinanceData 用于在线下载 YahooFinanceCSVData 用于离线文件 使用旧的 API/格式 # 使用旧的 API/格式时：\ndata = bt.feeds.YahooFinanceData( ... version=\u0026#39;\u0026#39;, ... ) 离线 Yahoo 数据源实例化如下：\ndata = bt.feeds.YahooFinanceCSVData( ... version=\u0026#39;\u0026#39;, ... ) 在线服务可能会恢复（服务在没有任何公告的情况下被停用，也可能会恢复）。\n或者，针对变更前下载的离线文件，也可以这样做：\ndata = bt.feeds.YahooLegacyCSV( ... ... ) 新的 YahooLegacyCSV 简化了 version='' 的用法。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/11-datafeed-yahoo/","section":"教程","summary":"2017 年 5 月，Yahoo 停用了原有的 CSV 格式历史数据下载 API。随后新的 API（这里称为 v7）被标准化并已实现，也带来了 CSV 下载格式的变化。\n使用 v7 API/格式 # 从 1.9.49.116 版本开始，这是默认行为。直接使用：\n","title":"Yahoo 数据源接入说明","type":"docs"},{"content":" 本节介绍如何使用技术指标作为入场和出场信号。我们将用简单移动平均线（SMA）作为演示指标。\n交易规则 # 交易规则如下：\n入场条件： 收盘价大于最新 SMA 值时买入 出场条件： 持有头寸时，若收盘价小于 SMA 则卖出 前面章节的策略代码大部分可复用，本节重点关注如何计算技术指标。\n指标计算 # backtrader 的 indicators 模块内置了大量技术指标，如 SMA 的计算：\nself.sma = bt.indicators.MovingAverageSimple(self.datas[0], period=self.params.maperiod) 如上代码中参数 self.params.maperiod 就是 SMA 的均线周期。\n注：如果安装了 TA-Lib，backtrader 也集成了其支持，详见 指标-TALib。\n条件判断 # 现在基于 self.sma 判断进出场条件。\n为了简化代码，这里只考虑 SMA 的判断逻辑，在完整实例中会包含所有情况。\n入场判断：\nself.dataclose[0] \u0026gt; self.sma[0] 出场判断：\nself.dataclose[0] \u0026lt; self.sma[0] 策略代码 # 起始现金 1000 货币单位。\nimport datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) import backtrader as bt 策略部分：\nclass TestStrategy(bt.Strategy): params = ( (\u0026#39;maperiod\u0026#39;, 15), ) def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close self.order = None self.buyprice = None self.buycomm = None self.sma = bt.indicators.SimpleMovingAverage( self.datas[0], period=self.params.maperiod) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.log(\u0026#39;BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: self.log(\u0026#39;SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log(\u0026#39;Order Canceled/Margin/Rejected\u0026#39;) self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log(\u0026#39;OPERATION PROFIT, GROSS %.2f, NET %.2f\u0026#39; % (trade.pnl, trade.pnlcomm)) def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) if self.order: return if not self.position: if self.dataclose[0] \u0026gt; self.sma[0]: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.buy() else: if self.dataclose[0] \u0026lt; self.sma[0]: self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.sell() if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() cerebro.addstrategy(TestStrategy) modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, \u0026#39;../../datas/orcl-1995-2014.txt\u0026#39;) data = bt.feeds.YahooFinanceCSVData( dataname=datapath, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), reverse=False) cerebro.adddata(data) cerebro.broker.setcash(1000.0) cerebro.addsizer(bt.sizers.FixedSize, stake=10) cerebro.broker.setcommission(commission=0.0) print(\u0026#39;Starting Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) cerebro.run() print(\u0026#39;Final Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) 输出：\nStarting Portfolio Value: 1000.00 2000-01-24, Close, 25.55 2000-01-25, Close, 26.61 2000-01-25, BUY CREATE, 26.61 2000-01-26, BUY EXECUTED, Size 10, Price: 26.76, Cost: 267.60, Commission 0.00 2000-01-26, Close, 25.96 2000-01-27, Close, 24.43 2000-01-27, SELL CREATE, 24.43 2000-01-28, SELL EXECUTED, Size 10, Price: 24.28, Cost: 242.80, Commission 0.00 2000-01-28, OPERATION PROFIT, GROSS -24.80, NET -24.80 ... ... ... 2000-12-20, SELL CREATE, 26.88 2000-12-21, SELL EXECUTED, Size 10, Price: 26.23, Cost: 262.30, Commission 0.00 2000-12-21, OPERATION PROFIT, GROSS -20.60, NET -20.60 2000-12-21, Close, 27.82 2000-12-21, BUY CREATE, 27.82 2000-12-22, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.00 2000-12-22, Close, 30.06 2000-12-26, Close, 29.17 2000-12-27, Close, 28.94 2000-12-28, Close, 29.29 2000-12-29, Close, 27.41 2000-12-29, SELL CREATE, 27.41 Final Portfolio Value: 973.90 这次投资组合亏损了。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/09-indicators/","section":"教程","summary":" 本节介绍如何使用技术指标作为入场和出场信号。我们将用简单移动平均线（SMA）作为演示指标。\n交易规则 # 交易规则如下：\n","title":"内置技术指标的应用","type":"docs"},{"content":"Fyne 通常会通过选择驱动程序和配置来适当地为目标平台配置您的应用程序。以下构建标签得到支持，可帮助您的开发。例如，如果您希望在桌面计算机上模拟移动应用程序，您可以使用以下命令：\ngo run -tags mobile main.go 标签 描述 debug 显示调试信息，包括视觉布局，以帮助理解您的应用。 gles 强制使用嵌入式 OpenGL (GLES) 而不是完整的 OpenGL。这通常由目标设备控制，通常不需要。 hints 显示开发者提示以进行改进或优化。使用 hints 运行时，当您的应用程序不遵循材料设计或其他建议时，将记录下来。 mobile 此标签在模拟的移动窗口中运行应用程序。当您想要在不编译和安装到设备的情况下预览您的应用在移动平台上的外观时非常有用。 no_animations 禁用标准控件和容器中的非必要动画。 no_emoji 不包含嵌入的 emoji 字体。这将在您的应用中禁用 emoji，但会使二进制文件更小。 no_native_menus 此标志专门用于 macOS，表示应用程序不应使用 macOS 原生菜单。相反，菜单将在应用程序窗口内显示。在 macOS 上测试应用程序以模拟 Windows 或 Linux 上的行为时最有用。 ","date":"2024-12-27","externalUrl":null,"permalink":"/docs/gofyne/02-explore/11-compiling/","section":"教程","summary":"Fyne 通常会通过选择驱动程序和配置来适当地为目标平台配置您的应用程序。以下构建标签得到支持，可帮助您的开发。例如，如果您希望在桌面计算机上模拟移动应用程序，您可以使用以下命令：\n","title":"编译选项","type":"docs"},{"content":"自fyne命令v2.1.0版本发布以来，我们支持一个元数据文件，允许你在仓库中存储有关你的应用的信息。这个文件是可选的，但可以帮助避免在每个包和发布命令中记住特定的构建参数。\n文件应该命名为FyneApp.toml，位于你运行fyne命令的目录中（通常是main包）。文件的内容如下：\nWebsite = \u0026#34;https://example.com\u0026#34; [Details] Icon = \u0026#34;Icon.png\u0026#34; Name = \u0026#34;My App\u0026#34; ID = \u0026#34;com.example.app\u0026#34; Version = \u0026#34;1.0.0\u0026#34; Build = 1 文件的顶部部分是元数据，如果你将你的应用上传到https://apps.fyne.io列表页面时会使用，因此它是可选的。[Details]部分包含了其他应用商店和操作系统在发布过程中使用的有关你的应用的数据。如果找到了这个文件，fyne工具将会使用它，很多强制性的命令参数如果元数据存在则不是必需的。你仍然可以使用命令行参数覆盖这些值。\n","date":"2024-11-15","externalUrl":null,"permalink":"/docs/gofyne/01-started/11-metadata/","section":"教程","summary":"自fyne命令v2.1.0版本发布以来，我们支持一个元数据文件，允许你在仓库中存储有关你的应用的信息。这个文件是可选的，但可以帮助避免在每个包和发布命令中记住特定的构建参数。\n","title":"应用元数据 Metadata","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/11-commission-schemes/","section":"教程","summary":"","title":"Commission","type":"docs"},{"content":"注意，需要安装 pandas 及其依赖项。支持 Pandas Dataframe 很重要，许多人依赖 Pandas 提供的解析功能来处理不同数据源（包括 CSV）。\n参数声明 # 注意\n以下只是参数声明，不要盲目复制。请参见下面的实际用法示例：\nclass PandasData(feed.DataBase): \u0026#39;\u0026#39;\u0026#39; ``dataname`` 参数继承自 ``feed.DataBase`` 是 pandas DataFrame \u0026#39;\u0026#39;\u0026#39; params = ( # datetime 的可能值（必须始终存在） # None : datetime 是 Pandas Dataframe 中的 \u0026#34;index\u0026#34; # -1 : 自动检测位置或大小写相同的名称 # \u0026gt;= 0 : pandas dataframe 中列的数值索引 # string : pandas dataframe 中的列名（作为索引） (\u0026#39;datetime\u0026#39;, None), # 下面是可能的值： # None : 列不存在 # -1 : 自动检测位置或大小写相同的名称 # \u0026gt;= 0 : pandas dataframe 中列的数值索引 # string : pandas dataframe 中的列名（作为索引） (\u0026#39;open\u0026#39;, -1), (\u0026#39;high\u0026#39;, -1), (\u0026#39;low\u0026#39;, -1), (\u0026#39;close\u0026#39;, -1), (\u0026#39;volume\u0026#39;, -1), (\u0026#39;openinterest\u0026#39;, -1), ) 上述 PandasData 类的片段展示了以下关键点：\n实例化时，dataname 参数包含 Pandas Dataframe\n该参数继承自基类 feed.DataBase\n新参数使用 DataSeries 中常规字段的名称，遵循以下约定：\ndatetime (默认: None)\nNone: datetime 是 Pandas Dataframe 中的“索引” -1: 自动检测位置或大小写相同的名称 = 0: pandas dataframe 中列的数值索引\nstring: pandas dataframe 中的列名（作为索引） open、high、low、close、volume、openinterest (默认: -1)\nNone: 列不存在 -1: 自动检测位置或大小写相同的名称 = 0: pandas dataframe 中列的数值索引\nstring: pandas dataframe 中的列名（作为索引） 一个小示例，加载经 Pandas 解析的标准 2006 示例数据，而非由 backtrader 直接解析。\n运行示例代码，使用 CSV 数据中的标题行：\n$ ./panda-test.py -------------------------------------------------- Open High Low Close Volume OpenInterest Date 2006-01-02 3578.73 3605.95 3578.73 3604.33 0 0 2006-01-03 3604.08 3638.42 3601.84 3614.34 0 0 2006-01-04 3615.23 3652.46 3615.23 3652.46 0 0 相同的代码，但告诉脚本跳过标题：\n$ ./panda-test.py --noheaders -------------------------------------------------- 1 2 3 4 5 6 0 2006-01-02 3578.73 3605.95 3578.73 3604.33 0 0 2006-01-03 3604.08 3638.42 3601.84 3614.34 0 0 2006-01-04 3615.23 3652.46 3615.23 3652.46 0 0 第二次运行时，使用 pandas.read_csv：\n跳过第一行（skiprows 设置为 1） 不查找标题行（header 设置为 None） backtrader 的 Pandas 支持会尝试自动检测列名，否则使用数值索引，以提供最佳匹配。\n以下图表展示了成功的结果。Pandas Dataframe 已正确加载（在两种情况下均如此）。\n示例代码：\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import backtrader as bt import backtrader.feeds as btfeeds import pandas def runstrat(): args = parse_args() # 创建 cerebro 实体 cerebro = bt.Cerebro(stdstats=False) # 添加策略 cerebro.addstrategy(bt.Strategy) # 获取 pandas dataframe datapath = (\u0026#39;../../datas/2006-day-001.txt\u0026#39;) # 模拟在请求 noheaders 时不存在头行 skiprows = 1 if args.noheaders else 0 header = None if args.noheaders else 0 dataframe = pandas.read_csv(datapath, skiprows=skiprows, header=header, parse_dates=True, index_col=0) if not args.noprint: print(\u0026#39;--------------------------------------------------\u0026#39;) print(dataframe) print(\u0026#39;--------------------------------------------------\u0026#39;) # 将其传递给 backtrader 数据源并添加到 cerebro data = bt.feeds.PandasData(dataname=dataframe) cerebro.adddata(data) # 运行所有内容 cerebro.run() # 绘制结果 cerebro.plot(style=\u0026#39;bar\u0026#39;) def parse_args(): parser = argparse.ArgumentParser( description=\u0026#39;Pandas 测试脚本\u0026#39;) parser.add_argument(\u0026#39;--noheaders\u0026#39;, action=\u0026#39;store_true\u0026#39;, default=False, required=False, help=\u0026#39;不使用头行\u0026#39;) parser.add_argument(\u0026#39;--noprint\u0026#39;, action=\u0026#39;store_true\u0026#39;, default=False, help=\u0026#39;打印 dataframe\u0026#39;) return parser.parse_args() if __name__ == \u0026#39;__main__\u0026#39;: runstrat() ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/12-datafeed-pandas/","section":"教程","summary":"注意，需要安装 pandas 及其依赖项。支持 Pandas Dataframe 很重要，许多人依赖 Pandas 提供的解析功能来处理不同数据源（包括 CSV）。\n参数声明 # 注意\n","title":"Pandas 数据源使用示例","type":"docs"},{"content":" 通过 print 输出每个 bar 的信息不利于阅读，图表视觉效果更直观。backtrader 内置了绘图能力，一行代码即可：\ncerebro.plot() 需在 cerebro.run() 之后调用，且绘图功能依赖 matplotlib。\n演示 # 为了展示绘图功能，我们将添加以下指标：\nEMA（指数移动平均线），默认与数据一起绘制 WMA（加权移动平均线），配置在子图绘制 StochasticSlow（慢速随机指标），使用默认设置 MACD，使用默认设置 ATR，配置为不绘制 RSI，使用默认设置 在 RSI 上叠加 SMA 指标 在策略的 __init__ 方法中添加的所有内容：\n# Indicators for the plotting show bt.indicators.ExponentialMovingAverage(self.datas[0], period=25) bt.indicators.WeightedMovingAverage(self.datas[0], period=25).subplot = True bt.indicators.StochasticSlow(self.datas[0]) bt.indicators.MACDHisto(self.datas[0]) rsi = bt.indicators.RSI(self.datas[0]) bt.indicators.SmoothedMovingAverage(rsi, period=10) bt.indicators.ATR(self.datas[0]).plot = False 即使指标没有赋值到策略成员变量（如 self.sma = ...），它们也会被注册到策略中，成为图表的一部分。\n示例中，只有RSI被添加到临时变量rsi中，其目的是要在其上创建一个 SmoothedMovingAverage。\nfrom __future__ import (absolute_import, division, print_function, unicode_literals) import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) import backtrader as bt class TestStrategy(bt.Strategy): params = ( (\u0026#39;maperiod\u0026#39;, 15), ) def log(self, txt, dt=None): dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close self.order = None self.buyprice = None self.buycomm = None self.sma = bt.indicators.SimpleMovingAverage( self.datas[0], period=self.params.maperiod) # Indicators for the plotting show bt.indicators.ExponentialMovingAverage(self.datas[0], period=25) bt.indicators.WeightedMovingAverage(self.datas[0], period=25, subplot=True) bt.indicators.StochasticSlow(self.datas[0]) bt.indicators.MACDHisto(self.datas[0]) rsi = bt.indicators.RSI(self.datas[0]) bt.indicators.SmoothedMovingAverage(rsi, period=10) bt.indicators.ATR(self.datas[0], plot=False) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.log(\u0026#39;BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: self.log(\u0026#39;SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f\u0026#39; % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log(\u0026#39;Order Canceled/Margin/Rejected\u0026#39;) self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log(\u0026#39;OPERATION PROFIT, GROSS %.2f, NET %.2f\u0026#39; % (trade.pnl, trade.pnlcomm)) def next(self): self.log(\u0026#39;Close, %.2f\u0026#39; % self.dataclose[0]) if self.order: return if not self.position: if self.dataclose[0] \u0026gt; self.sma[0]: self.log(\u0026#39;BUY CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.buy() else: if self.dataclose[0] \u0026lt; self.sma[0]: self.log(\u0026#39;SELL CREATE, %.2f\u0026#39; % self.dataclose[0]) self.order = self.sell() if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() cerebro.addstrategy(TestStrategy) modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, \u0026#39;../../datas/orcl-1995-2014.txt\u0026#39;) data = bt.feeds.YahooFinanceCSVData( dataname=datapath, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), reverse=False) cerebro.adddata(data) cerebro.broker.setcash(1000.0) cerebro.addsizer(bt.sizers.FixedSize, stake=10) cerebro.broker.setcommission(commission=0.0) print(\u0026#39;Starting Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) cerebro.run() print(\u0026#39;Final Portfolio Value: %.2f\u0026#39; % cerebro.broker.getvalue()) # Plot the result cerebro.plot() 执行后的输出：\nStarting Portfolio Value: 1000.00 2000-02-18, Close, 27.61 2000-02-22, Close, 27.97 2000-02-22, BUY CREATE, 27.97 2000-02-23, BUY EXECUTED, Size 10, Price: 28.38, Cost: 283.80, Commission 0.00 2000-02-23, Close, 29.73 ... ... ... 2000-12-21, BUY CREATE, 27.82 2000-12-22, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.00 2000-12-22, Close, 30.06 2000-12-26, Close, 29.17 2000-12-27, Close, 28.94 2000-12-28, Close, 29.29 2000-12-29, Close, 27.41 2000-12-29, SELL CREATE, 27.41 Final Portfolio Value: 981.00 交易逻辑未修改，结果与上节一样，图表如下：\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/10-plotting/","section":"教程","summary":" 通过 print 输出每个 bar 的信息不利于阅读，图表视觉效果更直观。backtrader 内置了绘图能力，一行代码即可：\n","title":"回测结果的可视化输出","type":"docs"},{"content":"按照打包页面所述打包图形应用程序会提供一个可以直接分享或分发的文件或捆绑包。然而，签名并上传到应用商店和市场是一个需要特定平台配置的额外步骤，我们将在这个页面中介绍。\n在这些步骤中，我们将使用fyne命令行工具的一部分新工具。fyne release步骤处理为每个商店的签名和准备，但参数因平台而异，我们将在下面看到。\nmacOS AppStore（自fyne 1.4.2起） # 先决条件：\n运行macOS和Xcode的Apple mac Apple开发者账户 Mac AppStore 应用证书 Mac AppStore 安装程序证书 从 AppStore 下载的 Apple Transporter 应用 在AppStore Connect上为要上传的构建准备好你的应用/版本。\n为发布捆绑完成的应用：\n$ fyne release -appID com.example.myapp -appVersion 1.0 -appBuild 1 -category games 将.pkg拖到Transporter上并点击“交付”。\n返回到AppStore Connect网站，选择你的构建版本进行发布，并提交审核。\n谷歌Play商店（Android） # 先决条件：\nGoogle Play控制台账户 分发密钥库（创建指南在 android文档） 在Google Play控制台上为要上传的构建准备好你的应用/版本。关闭“Play应用签名”选项，因为我们自己管理它。\n为发布捆绑完成的应用：\n$ fyne release -os android -appID com.example.myapp -appVersion 1.0 -appBuild 1 将.apk文件拖到Play控制台中应用版本页面的构建投放区。\n开始新版本的推出。\niOS AppStore（自fyne 1.4.1起） # 先决条件：\n运行macOS和Xcode的Apple mac Apple开发者账户 iOS AppStore 分发证书 从 AppStore 下载的Apple Transporter应用 在AppStore Connect上为要上传的构建准备好你的应用/版本。\n为发布捆绑完成的应用：\n$ fyne release -os ios -appID com.example.myapp -appVersion 1.0 -appBuild 1 将.ipa拖到Transporter上并点击“交付”。\n返回到AppStore Connect网站，选择你的构建版本进行发布，并提交审核。\n","date":"2024-11-18","externalUrl":null,"permalink":"/docs/gofyne/01-started/12-distribution/","section":"教程","summary":"按照打包页面所述打包图形应用程序会提供一个可以直接分享或分发的文件或捆绑包。然而，签名并上传到应用商店和市场是一个需要特定平台配置的额外步骤，我们将在这个页面中介绍。\n","title":"发布到应用商店","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/12-analyzers/","section":"教程","summary":"","title":"Analyzer","type":"docs"},{"content":" 每个市场或交易品种都有不同的节奏，没有一种策略适合所有情况。\n前面的示例中，SMA 周期默认值为 15。这是一个可优化的参数，通过改变参数值找出最适合当前市场的设置。\n注意：不要过度优化。如果交易思路不健全，优化可能产生仅对回测数据有效的正面结果。\n下面示例优化 SMA 的周期，为清晰起见删除了与买卖订单相关的输出。\n示例如下：\nimport datetime #For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) import backtrader as bt class TestStrategy(bt.Strategy): params = ( (\u0026#39;maperiod\u0026#39;, 15), (\u0026#39;printlog\u0026#39;, False), ) def log(self, txt, dt=None, doprint=False): if self.params.printlog or doprint: dt = dt or self.datas[0].datetime.date(0) print(\u0026#39;%s, %s\u0026#39; % (dt.isoformat(), txt)) def __init__(self): self.dataclose = self.datas[0].close self.order = None self.buyprice = None self.buycomm = None self.sma = bt.indicators.SimpleMovingAverage( self.datas[0], period=self.params.maperiod) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.buyprice = order.executed.price self.buycomm = order.executed.comm else: self.bar_executed = len(self) self.order = None def notify_trade(self, trade): if not trade.isclosed: return def next(self): if self.order: return if not self.position: if self.dataclose[0] \u0026gt; self.sma[0]: self.order = self.buy() else: if self.dataclose[0] \u0026lt; self.sma[0]: self.order = self.sell() def stop(self): self.log(\u0026#39;(MA Period %2d) Ending Value %.2f\u0026#39; % (self.params.maperiod, self.broker.getvalue()), doprint=True) if __name__ == \u0026#39;__main__\u0026#39;: cerebro = bt.Cerebro() strats = cerebro.optstrategy( TestStrategy, maperiod=range(10, 31)) modpath = os.path.dirname(os.path.abspath(sys.argv[0])) datapath = os.path.join(modpath, \u0026#39;../../datas/orcl-1995-2014.txt\u0026#39;) data = bt.feeds.YahooFinanceCSVData( dataname=datapath, fromdate=datetime.datetime(2000, 1, 1), todate=datetime.datetime(2000, 12, 31), reverse=False) cerebro.adddata(data) cerebro.broker.setcash(1000.0) cerebro.addsizer(bt.sizers.FixedSize, stake=10) cerebro.broker.setcommission(commission=0.0) cerebro.run(maxcpus=1) 优化时不再调用 addstrategy，而是使用 optstrategy，传递参数的范围而非单一值。\n代码中添加了 stop 方法，回测结束时自动调用，用于打印最终净值。\n运行程序后，系统为每个参数值执行一次回测。\n输出如下：\n2000-12-29, (MA Period 10) Ending Value 880.30 2000-12-29, (MA Period 11) Ending Value 880.00 2000-12-29, (MA Period 12) Ending Value 830.30 2000-12-29, (MA Period 13) Ending Value 893.90 2000-12-29, (MA Period 14) Ending Value 896.90 2000-12-29, (MA Period 15) Ending Value 973.90 2000-12-29, (MA Period 16) Ending Value 959.40 2000-12-29, (MA Period 17) Ending Value 949.80 2000-12-29, (MA Period 18) Ending Value 1011.90 2000-12-29, (MA Period 19) Ending Value 1041.90 2000-12-29, (MA Period 20) Ending Value 1078.00 2000-12-29, (MA Period 21) Ending Value 1058.80 2000-12-29, (MA Period 22) Ending Value 1061.50 2000-12-29, (MA Period 23) Ending Value 1023.00 2000-12-29, (MA Period 24) Ending Value 1020.10 2000-12-29, (MA Period 25) Ending Value 1013.30 2000-12-29, (MA Period 26) Ending Value 998.30 2000-12-29, (MA Period 27) Ending Value 982.20 2000-12-29, (MA Period 28) Ending Value 975.70 2000-12-29, (MA Period 29) Ending Value 983.30 2000-12-29, (MA Period 30) Ending Value 979.80 结果 # 周期小于 18：策略亏损 周期 18 到 26（含）：策略盈利 周期超过 26：策略再次亏损 对于这个策略和数据集，最优参数是均线周期为 20，盈利 78.00 单位货币（7.8%）。\n","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/03-quickstart/11-optimization/","section":"教程","summary":" 每个市场或交易品种都有不同的节奏，没有一种策略适合所有情况。\n前面的示例中，SMA 周期默认值为 15。这是一个可优化的参数，通过改变参数值找出最适合当前市场的设置。\n","title":"策略参数优化与调参","type":"docs"},{"content":" AbstractDataBase # 数据行（Lines）:\nclose low high open volume openinterest datetime 参数（Params）:\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) BacktraderCSVData # 解析用于测试的自定义 CSV 数据。\n特定参数：\ndataname: 要解析的文件名或类文件对象 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) CSVDataBase # 用于实现 CSV 数据源的基类。\n负责打开文件、读取行并将其标记化。子类只需重写 _loadline(tokens) 方法。\n数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) Chainer # 用于链式连接数据。\n数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) DataClone # 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) DataFiller # 使用基础数据源的信息填充数据中的空隙。\n参数：\nfill_price (def: None): 如果为 None，将使用上一条数据的收盘价；否则使用传递的值（例如 ‘NaN’） fill_vol (def: NaN): 用于填充缺失数据的交易量 fill_oi (def: NaN): 用于填充缺失数据的未平仓合约量 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) fill_price (None) fill_vol (nan) fill_oi (nan) DataFilter # 过滤给定数据源中的数据行。除 DataBase 的标准参数外，还接受 funcfilter 参数（任何可调用对象）。\n逻辑：\nfuncfilter 随基础数据源一起调用 返回值 True：当前数据行将被使用 返回值 False：当前数据行将被丢弃 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) funcfilter (None) GenericCSVData # 根据定义的参数解析 CSV 文件。\n特定参数（或特定含义）：\ndataname: 要解析的文件名或类文件对象 lines 参数（datetime, open, high\u0026hellip;）取数值 值为 -1 表示 CSV 源中不存在该字段 如果 time 存在（参数 time \u0026gt;= 0），源包含分开的日期和时间字段，将合并 参数：\nnullvalue: CSV 字段为空时使用的值 dtformat: 解析 datetime CSV 字段的格式。参见 Python strptime/strftime 文档了解格式说明 如果指定数值： 1: Unix 时间戳（int），自 1970-01-01 以来的秒数 2: Unix 时间戳（float），自 1970-01-01 以来的秒数 如果传递可调用对象，它将接受一个字符串并返回 datetime.datetime 实例 tmformat: 解析 time CSV 字段的格式（默认不存在） 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) nullvalue (nan) dtformat (%Y-%m-%d %H:%M:%S) tmformat (%H:%M:%S) datetime (0) time (-1) open (1) high (2) low (3) close (4) volume (5) openinterest (6) IBData # Interactive Brokers 数据源\n支持参数 dataname 中的合约规格：\nTICKER # 股票类型和 SMART 交易所 TICKER-STK # 股票和 SMART 交易所 TICKER-STK-EXCHANGE # 股票 TICKER-STK-EXCHANGE-CURRENCY # 股票 TICKER-CFD # CFD 和 SMART 交易所 TICKER-CFD-EXCHANGE # CFD TICKER-CDF-EXCHANGE-CURRENCY # 股票 TICKER-IND-EXCHANGE # 指数 TICKER-IND-EXCHANGE-CURRENCY # 指数 TICKER-YYYYMM-EXCHANGE # 期货 TICKER-YYYYMM-EXCHANGE-CURRENCY # 期货 TICKER-YYYYMM-EXCHANGE-CURRENCY-MULT # 期货 TICKER-FUT-EXCHANGE-CURRENCY-YYYYMM-MULT # 期货 TICKER-YYYYMM-EXCHANGE-CURRENCY-STRIKE-RIGHT # 期权 TICKER-YYYYMM-EXCHANGE-CURRENCY-STRIKE-RIGHT-MULT # 期权 TICKER-FOP-EXCHANGE-CURRENCY-YYYYMM-STRIKE-RIGHT # 期权 TICKER-FOP-EXCHANGE-CURRENCY-YYYYMM-STRIKE-RIGHT-MULT # 期权 CUR1.CUR2-CASH-IDEALPRO # 外汇 TICKER-YYYYMMDD-EXCHANGE-CURRENCY-STRIKE-RIGHT # 期权 TICKER-YYYYMMDD-EXCHANGE-CURRENCY-STRIKE-RIGHT-MULT # 期权 TICKER-OPT-EXCHANGE-CURRENCY-YYYYMMDD-STRIKE-RIGHT # 期权 TICKER-OPT-EXCHANGE-CURRENCY-YYYYMMDD-STRIKE-RIGHT-MULT # 期权 参数：\nsectype (默认: STK) exchange (默认: SMART) currency (默认: \u0026lsquo;\u0026rsquo;) historical (默认: False) what (默认: None) rtbar (默认: False) qcheck (默认: 0.5) backfill_start (默认: True) backfill (默认: True) backfill_from (默认: None)\nlatethrough (默认: False) tradename (默认: None) 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.5) calendar (None) sectype (STK) exchange (SMART) currency () rtbar (False) historical (False) what (None) useRTH (False) backfill_start (True) backfill (True) backfill_from (None) latethrough (False) tradename (None) InfluxDB # 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) host (127.0.0.1) port (8086) username (None) password (None) database (None) startdate (None) high (high_p) low (low_p) open (open_p) close (close_p) volume (volume) ointerest (oi) MT4CSVData # 解析 Metatrader4 历史中心导出的 CSV 文件。\n特定参数（或特定含义）：\ndataname: 要解析的文件名或类文件对象 基于 GenericCSVData，只修改了参数 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) nullvalue (nan) dtformat (%Y.%m.%d) tmformat (%H:%M) datetime (0) time (1) open (2) high (3) low (4) close (5) volume (6) openinterest (-1) OandaData # 参数：\nqcheck (默认: 0.5) historical (默认: False) backfill_start (默认: True) backfill (默认: True) backfill_from (默认: None) bidask (默认: True) useask (默认: False) includeFirst (默认: True) reconnect (默认: True) reconnections (默认: -1) reconntimeout (默认: 5.0) 时间框架和压缩映射符合 OANDA API 开发者指南中的定义：\n(TimeFrame.Seconds, 5): \u0026#39;S5\u0026#39;, (TimeFrame.Seconds, 10): \u0026#39;S10\u0026#39;, (TimeFrame.Seconds, 15): \u0026#39;S15\u0026#39;, (TimeFrame.Seconds, 30): \u0026#39;S30\u0026#39;, (TimeFrame.Minutes, 1): \u0026#39;M1\u0026#39;, (TimeFrame.Minutes, 2): \u0026#39;M3\u0026#39;, (TimeFrame.Minutes, 3): \u0026#39;M3\u0026#39;, (TimeFrame.Minutes, 4): \u0026#39;M4\u0026#39;, (TimeFrame.Minutes, 5): \u0026#39;M5\u0026#39;, (TimeFrame.Minutes, 10): \u0026#39;M10\u0026#39;, (TimeFrame.Minutes, 15): \u0026#39;M15\u0026#39;, (TimeFrame.Minutes, 30): \u0026#39;M30\u0026#39;, (TimeFrame.Minutes, 60): \u0026#39;H1\u0026#39;, (TimeFrame.Minutes, 120): \u0026#39;H2\u0026#39;, (TimeFrame.Minutes, 180): \u0026#39;H3\u0026#39;, (TimeFrame.Minutes, 240): \u0026#39;H4\u0026#39;, (TimeFrame.Minutes, 360): \u0026#39;H6\u0026#39;, (TimeFrame.Minutes, 480): \u0026#39;H8\u0026#39;, (TimeFrame.Days, 1): \u0026#39;D\u0026#39;, (TimeFrame.Weeks, 1): \u0026#39;W\u0026#39;, (TimeFrame.Months, 1): \u0026#39;M\u0026#39;, 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.5) calendar (None) historical (False) backfill_start (True) backfill (True) backfill_from (None) bidask (True) useask (False) includeFirst (True) reconnect (True) reconnections (-1) reconntimeout (5.0) PandasData # 使用 Pandas DataFrame 作为数据源。\n参数：\nnocase (默认: True): 列名匹配不区分大小写 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) nocase (True) datetime (None) open (-1) high (-1) low (-1) close (-1) volume (-1) openinterest (-1) PandasDirectData # 使用 Pandas DataFrame 作为数据源，直接迭代 itertuples 返回的元组。\n数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) datetime (0) open (1) high (2) low (3) close (4) volume (5) openinterest (6) Quandl # 从 Quandl 服务器下载数据。\n特定参数（或特定含义）：\ndataname: 要下载的代码（如 \u0026lsquo;YHOO\u0026rsquo;） baseurl: 服务器 URL proxies: 下载时使用的代理字典，如 {'http': 'http://myproxy.com'} buffered: 若为 True，整个 socket 连接在解析前缓存在本地 reverse: Quandl 默认返回降序（最新的在前）。若为 True，请求升序（最旧的在前） adjclose: 是否使用股息/拆股调整后的收盘价，并据此调整所有值 apikey: 使用的 API 密钥（如果需要） dataset: 标识要查询的数据集，默认为 WIKI 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) reverse (True) adjclose (True) round (False) decimals (2) baseurl (https://www.quandl.com/api/v3/datasets) proxies ({}) buffered (True) apikey (None) dataset (WIKI) QuandlCSV # 解析预先下载的 Quandl CSV 数据源（或符合 Quandl 格式的本地 CSV 文件）。\n特定参数：\ndataname: 要解析的文件名或类文件对象 reverse (默认: False): 假设本地文件在下载过程中已被反转 adjclose (默认: True): 是否使用股息/拆股调整后的收盘价，并据此调整所有值 round (默认: False): 调整收盘价后是否四舍五入到指定小数位数 decimals (默认: 2): 四舍五入的小数位数 数据行： # close\nlow high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) reverse (False) adjclose (True) round (False) decimals (2) RollOver # 满足条件时切换到下一个期货合约。\n参数：\ncheckdate (默认: None): 必须是具有以下签名的可调用对象： checkdate(dt, d): dt: datetime.datetime 对象 d: 当前活跃期货的数据源 返回值：\nTrue: 可以切换到下一个期货\nFalse: 不能切换\ncheckcondition (默认: None): 仅当 checkdate 返回 True 时才会调用此方法。如果为 None，内部评估为 True（执行切换）。否则，必须是具有以下签名的可调用对象：\ncheckcondition(d0, d1): d0: 当前活跃期货的数据源 d1: 下一个到期的数据源 返回值：\nTrue: 切换到下一个期货 False: 不能切换 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) checkdate (None) checkcondition (None) SierraChartCSVData # 解析 Sierra Chart 导出的 CSV 文件。\n特定参数（或特定含义）：\ndataname: 要解析的文件名或类文件对象 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) nullvalue (nan) dtformat (%Y/%m/%d) tmformat (%H:%M:%S) datetime (0) time (-1) open (1) high (2) low (3) close (4) volume (5) openinterest (6) VCData # VisualChart 数据源。\n参数：\nqcheck (默认: 0.5): 唤醒默认超时，通知重采样器/重放器当前 Bar 可检查是否应交付 historical (默认: False): 如果未提供 todate 且设置为 True，强制仅进行历史下载。提供 todate 时效果相同。 milliseconds (默认: True): Visual Chart 构建的 Bar 格式为 HH:MM:59.999000。若为 True，将添加一毫秒使其变为 HH:MM + 1:00.000000 tradename (默认: None): 连续期货本身不能交易，但适合数据跟踪。提供此参数时，指定当前期货名称作为交易资产。例如： 001ES -\u0026gt; ES-Mini 连续期货作为 dataname ESU16 -\u0026gt; ES-Mini 2016-09，作为 tradename 时为交易资产 usetimezones (默认: True): Visual Chart 提供的时区信息可将日期时间转换为市场时间。某些市场（如 096）需要特殊的时区覆盖。若为 True，尝试使用 pytz 进行时区转换。禁用时移除时区处理（可能减少负载）。 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.5) calendar (None) historical (False) millisecond (True) tradename (None) usetimezones (True) VChartCSVData # 解析 VisualChart 导出的 CSV 文件。\n特定参数（或特定含义）：\ndataname: 要解析的文件名或类文件对象 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) VChartData # 支持 Visual Chart 二进制磁盘文件的每日和日内格式。\n注：\ndataname: 文件名或打开的类文件对象 如果传入类文件对象，使用 timeframe 参数确定时间框架；否则使用文件扩展名（.fd 为每日，.min 为日内）。\n数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) VChartFile # 支持 Visual Chart 二进制磁盘文件的每日和日内格式。\n注：\ndataname: Visual Chart 显示的市场代码。例如：015ES 表示 EuroStoxx 50 连续期货 数据行：\nclose low high open volume openinterest datetime 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) YahooFinanceCSVData # 解析预先下载的 Yahoo CSV 数据源（或符合 Yahoo 格式的本地 CSV 文件）。\n特定参数：\ndataname: 要解析的文件名或类文件对象 reverse (默认: False): 假设本地文件在下载过程中已被反转 adjclose (默认: True): 是否使用股息/拆股调整后的收盘价，并据此调整所有值 adjvolume (默认: True): 若 adjclose 也为 True，则调整交易量 round (默认: True): 调整收盘价后是否四舍五入到指定小数位数 roundvolume (默认: 0): 调整后将交易量四舍五入到指定小数位数 decimals (默认: 2): 四舍五入的小数位数 swapcloses (默认: False): [2018-11-16] 收盘价和调整后收盘价的顺序现已修复。保留此参数以防需要再次交换列的顺序。 数据行：\nclose low high open volume openinterest datetime adjclose 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) reverse (False) adjclose (True) adjvolume (True) round (True) decimals (2) roundvolume (False) swapcloses (False )\nYahooFinanceData # 从 Yahoo 服务器下载数据。\n特定参数（或特定含义）：\ndataname: 要下载的代码（如 \u0026lsquo;YHOO\u0026rsquo; 表示 Yahoo 自己的股票代码） proxies: 下载时使用的代理字典，如 {'http': 'http://myproxy.com'} period: 下载数据的时间框架，\u0026lsquo;w\u0026rsquo; 为每周，\u0026rsquo;m\u0026rsquo; 为每月 reverse: [2018-11-16] Yahoo 在线下载的最新版本已返回正确顺序，因此默认为 False adjclose: 是否使用股息/拆股调整后的收盘价，并据此调整所有值 urlhist: Yahoo Finance 的历史报价 URL，用于获取下载所需的 cookie urldown: 实际下载服务器的 URL retries: 获取 cookie 和下载数据的重试次数 数据行：\nclose low high open volume openinterest datetime adjclose 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) reverse (False) adjclose (True) adjvolume (True) round (True) decimals (2) roundvolume (False) swapcloses (False) proxies ({}) period (d) urlhist (https://finance.yahoo.com/quote/{}/history) urldown (https://query1.finance.yahoo.com/v7/finance/download) retries (3) YahooLegacyCSV # 加载 Yahoo 在 2017 年 5 月停止原始服务前下载的文件。\n数据行：\nclose low high open volume openinterest datetime adjclose 参数：\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) headers (True) separator (,) reverse (False) adjclose (True) adjvolume (True) round (True) decimals (2) roundvolume (False) swapcloses (False) version () ","date":"2026-05-20","externalUrl":null,"permalink":"/docs/backtrader/06-datafeed/13-datafeed-reference/","section":"教程","summary":"AbstractDataBase # 数据行（Lines）:\nclose low high open volume openinterest datetime 参数（Params）:\ndataname (None) name () compression (1) timeframe (5) fromdate (None) todate (None) sessionstart (None) sessionend (None) filters ([]) tz (None) tzinput (None) qcheck (0.0) calendar (None) BacktraderCSVData # 解析用于测试的自定义 CSV 数据。\n","title":"数据源 API 参考文档","type":"docs"},{"content":"使用Go进行跨平台编译被设计得非常简单 - 我们只需设置目标操作系统的环境变量GOOS（如果目标是不同的架构，还需要设置GOARCH）。不幸的是，当使用原生图形调用时，Fyne中的CGo使用使这变得有些复杂。\n从开发计算机编译 # 要跨编译Fyne应用程序，你还必须设置CGO_ENABLED=1，这告诉go启用C编译器（当目标平台与当前系统不同时，这通常是关闭的）。不幸的是，这意味着你必须为你将要编译的目标平台安装一个C编译器。安装适当的编译器后，你还需要设置CC环境变量来告诉Go使用哪个编译器。\n安装所需工具有许多方法 - 并且可以使用不同的工具。Fyne开发者推荐的配置是：\nGOOS（目标） CC 提供者 下载 备注 darwin o32-clang osxcross 来自github.com 你还需要安装macOS SDK（下载链接处有指引） windows x86_64-w64-mingw64-gcc mingw64 包管理器 对于macOS使用homebrew linux gcc 或 x86_64-linux-musl-gcc gcc 或 musl-cross cygwin 或 包管理器 musl-cross可从homebrew获取，提供linux gcc。你还需要为编译安装X11和mesa头文件。 设置上述环境变量后，你应该能够以通常的方式进行编译。如果进一步出现错误，很可能是由于缺少包。一些目标平台需要安装额外的库或头文件才能成功编译。\n使用虚拟环境 # 由于Linux系统能够轻松地交叉编译到macOS和Windows，因此当你不是从Linux开发时，使用虚拟化环境可能更简单。Docker镜像是复杂构建配置的有用工具，这也适用于Fyne。可以使用不同的工具。Fyne开发者推荐的工具是fyne-cross。它受到xgo的启发，并使用基于golang-cross镜像构建的docker镜像，该镜像包括了Windows的MinGW编译器和macOS SDK，以及Fyne的需求。\nfyne-cross允许为以下目标构建二进制文件并创建分发包：\nGOOS GOARCH darwin amd64 darwin 386 linux amd64 linux 386 linux arm64 linux arm windows amd64 windows 386 android amd64 android 386 android arm64 android arm ios freebsd amd64 freebsd arm64 注意：iOS编译仅支持在darwin主机上。\n要求 # go \u0026gt;= 1.13 docker 安装 # 你可以使用以下命令安装fyne-cross（需要Go 1.16或更高版本）：\ngo install github.com/fyne-io/fyne-cross@latest 对于Go的早期版本，你需要使用以下命令代替：\ngo get github.com/fyne-io/fyne-cross 使用方法 # fyne-cross \u0026lt;command\u0026gt; [options] 命令包括： darwin 为darwin OS构建和打包fyne应用程序 linux 为linux OS构建和打包fyne应用程序 windows 为windows OS构建和打包fyne应用程序 android 为android OS构建和打包fyne应用程序 ios 为iOS OS构建和打包fyne应用程序 freebsd 为freebsd OS构建和打包fyne应用程序 version 打印fyne-cross版本信息 使用 \u0026#34;fyne-cross \u0026lt;command\u0026gt; -help\u0026#34; 获取有关命令的更多信息。 通配符 # arch标志支持通配符，以防你想要针对指定GOOS的所有支持GOARCH进行编译\n示例：\nfyne-cross windows -arch=* 等同于\nfyne-cross windows -arch=amd64,386 示例 # 以下示例交叉编译并打包fyne示例应用程序\ngit clone https://github.com/fyne-io/examples.git cd examples 编译并打包主示例应用 # fyne-cross linux 注意：默认情况下，fyne-cross将在当前目录下编译包。\n上面的命令等同于：fyne-cross linux .\n编译并打包特定示例应用 # fyne-cross linux -output bugs ./cmd/bugs 通过上述方法，使用fyne-cross可以轻松地为多个平台交叉编译和打包Fyne应用程序，而无需手动配置每个平台的复杂环境，从而简化了跨平台开发和分发过程。\n","date":"2024-11-21","externalUrl":null,"permalink":"/docs/gofyne/01-started/13-cross-compile/","section":"教程","summary":"使用Go进行跨平台编译被设计得非常简单 - 我们只需设置目标操作系统的环境变量GOOS（如果目标是不同的架构，还需要设置GOARCH）。不幸的是，当使用原生图形调用时，Fyne中的CGo使用使这变得有些复杂。\n","title":"跨平台编译","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/13-observers/","section":"教程","summary":"","title":"Observer","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/14-sizers/","section":"教程","summary":"","title":"Sizer","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/15-livetrading/","section":"教程","summary":"","title":"实盘","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/16-plotting/","section":"教程","summary":"","title":"绘图","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/17-datetime/","section":"教程","summary":"","title":"日期时间","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/18-automated-running/","section":"教程","summary":"","title":"自动运行","type":"docs"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/19-articles/","section":"教程","summary":"","title":"官方文章","type":"docs"},{"content":"本策略是从 Renko 图表类型得到的灵感，一个非常简单的趋势突破策略。核心参数的只有两个，还有三个参数是于仓位管理相关。\n什么是 Renko？ # 在说明 renko 是什么之前，先从一个交易中的常见困扰开始。\n传统蜡烛图上下翻飞的小影线和反转 K 线常常让人困惑：这到底是正常回调，还是反转的开始？这些由微小波动形成的 \u0026ldquo;噪音\u0026rdquo;，是许多错误判断的源头。\n是否有办法过滤掉这些噪音呢？\n由此就可以引申出一个新的图表类型 - Renko，或者称作 \u0026ldquo;砖形图\u0026rdquo;，就是为了这个目标而生的。\n这种行情图抛弃了\u0026quot;时间\u0026quot; 的概念，专注于“纯粹的价格变动幅度”。我们需要的是预先设定一个固定的 \u0026ldquo;砖块大小\u0026rdquo;（比如股价的10元）。\n规则很简单：只有当价格运动超过一个砖块的大小，才会在图上形成一块新的砖；价格变动不足一个砖块时，图表会静止不动。\n价格以白涨和黑跌的阶梯状呈现出一种独特的行情图，低于设定阈值的波动都被视为无关紧要的杂波，不予显示。\n最终效果是图表因此变得极其清晰，趋势的延续、停滞或反转一目了然，让你能专注于市场的主要运动，不被琐碎的波动干扰判断。\n砖块大小 # Renko 中很重的点是如何确认每个砖块大小，常见的砖块大小策略主要有三种思路。\n一是采用固定价格数值，例如设定股票每个砖块为0.5元； 二是采用动态方法，如 ATR 作为基准，设定砖块大小为一个 ATR ，或固定个数计算，如当前价格/固定个数得到间隔； 三是采用百分比模式，例如设定砖块大小为当前资产价格的1%。 ATR 动态计算的方法是最受推崇的方法，因为它能随波动率调整。\nPython 绘制 # python 中有 mplfinance 支持绘制 renko 图表，代码如下所示：\nimport pandas as pd import mplfinance as mpf df = pd.read_csv(\u0026#34;datas/BTC_4h.csv\u0026#34;, index_col=\u0026#34;datetime\u0026#34;, parse_dates=True) df = df[\u0026#34;2025-10\u0026#34;:] renko_params = { \u0026#34;brick_size\u0026#34;: 2000, } mpf.plot(df, type=\u0026#34;renko\u0026#34;) renko_params = { \u0026#34;brick_size\u0026#34;: \u0026#34;atr\u0026#34;, } mpf.plot(df, type=\u0026#34;renko\u0026#34;, renko_params=renko_params) 固定砖块大小图表：\nATR 砖块大小：\n策略设计 # 基于 Renko 砖块突破的趋势连续性设计一个策略，核心两个参数：连续多个砖块开仓，反转连续多个砖块平仓。Renko 的砖块大小就通过 ATR 计算。\n仓位管理逻辑，每单交易的最大损失固定为 2%（24 小时市场），止损单价格在下单价格损失 3 个 ATR，杠杆最大不超过 1.5。\n这是一个非常简单的策略。\n回测优化：\n参数范围：开仓突破 1-5 个连续砖块，平仓连续 1-5 个砖块。 行情数据：BTC 1小时和 4 小时数据； 时间防伪：2020-01-01 到 2025-11-30； BTC 的 4 小时上回测。 # 不同参数走势图：\n不同评价标准的参数热力图：\n各方面指标都不错的参数组合（开仓 4，平仓 5）：\n所有参数按周再平衡表现：\nBTC 的 1 小时上回测。 # 不同参数走势图：\n不同评价标准的参数热力图：\n各方面指标都不错的参数组合（开仓 5，平仓 4）：\n所有参数按周再平衡表现：\n总体而言，1 小时上的表现稳定性比 4 小时要好。但无论是 1 小时还是 4 小时，最新一年的表现都比较拉垮。\n如果看 4 小时和 1 小时所有参数再平衡年度和月度表现：\n4 小时：\n1 小时:\n总结就是今年的行情走的非常令人恶心，太多假突破。\n","date":"2025-12-15","externalUrl":null,"permalink":"/posts/2025-12-15-renko-breakout-strategy/","section":"文章","summary":"本策略是从 Renko 图表类型得到的灵感，一个非常简单的趋势突破策略。核心参数的只有两个，还有三个参数是于仓位管理相关。\n什么是 Renko？ # 在说明 renko 是什么之前，先从一个交易中的常见困扰开始。\n","title":"策略1 - Renko ATR 突破","type":"posts"},{"content":"在做交易策略时，经常会碰到一个问题：同一种策略在不同阶段表现完全不一样。有时候波动大，你的仓位可能重了点；波动小，你又觉得仓位太轻，收益跟不上。\n为了让整体风险变得可控，很多机构会使用一种非常“实用主义”的方法——目标波动率（Target Volatility）。\n它的核心思想只有一句话：\n让你的组合一直保持在预设的年化波动率，不多也不少。\n这个想法其实很接近我们做 CTA、趋势策略时常说的“风险平价”，但目标波动率策略通常更简单、更直接，比较适合作为一个基础仓位管理模块。\n为什么需要目标波动率？ # 做交易时，可能都有这种体验：\n同一资产在不同年份的波动差距能大到离谱；震荡期仓位太重，会被洗到怀疑人生；单边大行情仓位太轻，看着涨但赚不到钱；策略性能的“稳定性”其实更多取决于仓位管理，而不是策略本身。\n目标波动率的出发点就是：既然波动率影响收益和风险，那干脆用波动率来动态调整仓位。\n波动高 → 仓位自动变轻 波动低 → 仓位自然变重 这样不用每天纠结现在是不是应该加仓或减仓，只需要按照公式让仓位自动收缩或扩张。\n策略原理 # 策略其实只有一个核心公式：\n目标仓位权重 = 目标年化波动率 / 当前年化波动率 例如，想把策略控制在 15% 的年化波动率，当前计算出来的波动率是 30%，那么你就应该只拿：15% / 30% = 0.5 倍仓位。\n如果你允许最大杠杆为 1.5，那么这个权重会自动裁剪：\n权重 = min(目标仓位权重, 最大杠杆) 整个策略就靠这个仓位调整来运行。\n使用 Backtrader 回测 # 我用了 SPY 这个品种回测了这个目标波动率的策略，策略的完整回测代码，请访问：voltarget.py。\n我这里讲解代码中的一些核心指标计算与交易逻辑。\n首先，波动率指标的计算，以当前的实际年化波动率作为基准。\nreturns = bt.ind.PercentChange(self.data.close, period=1) std_dev = bt.ind.StandardDeviation(returns, period=self.p.period) self.volatility = std_dev * np.sqrt(self.p.annual_factor) 通过计算的是 20 日的收益率标准差，并年化（乘上 √252）。这就是当前市场的“真实波动”。\n在交易部分按波动率动态调整仓位\ntarget_percent = min( self.p.target_vol / self.volatility[0], self.p.max_leverage ) target_size = self.broker.getvalue() / self.data.close[0] * target_percent self.order_target_size(target=target_size) 逻辑非常清晰：\ntarget_percent 就是我们上面讲的核心公式； 使用 order_target_size() 自动把仓位调整到目标仓位。 这让策略在波动小的时候自动加杠杆，在波动大时自动降杠杆，实现类似“风险再平衡”的效果。\n加载数据 # 用 YFinance 加载 SPY 标普 500 的历史数据。\ndf = yf.download( symbol, start=\u0026#34;2015-01-01\u0026#34;, end=\u0026#34;2025-11-30\u0026#34;, multi_level_index=False, auto_adjust=True, ) SPY 其实很适合这个策略，因为它大部分时间波动率稳定，偶尔波动巨大，用固定仓位很容易经历极端波动，而目标波动率策略可以让曲线变得相对 \u0026ldquo;平滑\u0026rdquo;。\n如下是在最大杠杆 2 的净值表现：\n调用方式：\npython script.py --symbol SPY --max-leverage 1.5 --target-volatility 2 输出评价指标：\n组合价值: 517871735.571006 夏普比率: 0.8689922727427503 最大回撤: 25.93392959742132 年化波动: 0.18463178961358762 净值绘图如下所示：\n红色的净值曲线是目标波动率的净值曲线，蓝色是基准。\n我回测了不同的市场，目标波动率策略在美股这种长期稳定向上，大部分时间波动率稳定，偶尔暴跌的指数有超额收益。而在 A 股和数字货币上并没有在降低波动率的同时还能保持收益。\n总体来说，这个是风险控制的策略，后续我会考虑放在一些趋势、对冲或者是多因子组合策略上，在降低策略风险的同时，努力提高策略的收益。\n7. 总结 # 目标波动率策略是一种“简单但好用”的工具。它不依赖方向、不依赖信号，而是利用波动率这个最基础的市场信息，动态调节仓位，实现一种自然的风险平衡。\n","date":"2025-12-09","externalUrl":null,"permalink":"/posts/2025-12-09-volatility-target-strategy/","section":"文章","summary":"在做交易策略时，经常会碰到一个问题：同一种策略在不同阶段表现完全不一样。有时候波动大，你的仓位可能重了点；波动小，你又觉得仓位太轻，收益跟不上。\n","title":"回测目标波动率策略","type":"posts"},{"content":"在金融市场，有一种被称为 \u0026ldquo;动量\u0026rdquo; 的现象。其核心理念很简单：如果一个资产（如股票）的价格正在上涨，那么它短期内更有可能继续上涨；同样，如果一个资产的价格正在下跌，它也更有可能继续下跌。这就像惯性一样。\n本文将基于这个现象具体介绍下动量策略，并回测这个策略在 数字货币市场上的表现。\n前言说明 # 我们先搞清楚为什么会出现动量。常见的说法有两个主要原因：\n信息传播需要时间：当一条好消息或坏消息出现时，并非所有市场参与者都能立刻知晓并采取行动，价格的调整会随着信息的逐步扩散而持续一段时间。\n机构交易行为：大型投资机构在买卖大量资产时，会不断执行买入操作，这个交易行为本身就会在市场上形成一个持续的趋势。\n基于此，发展出了两种主要的动量策略思路：\n时间序列动量：关注资产自身的历史表现。例如，买入过去一段时间表现绝对好的资产，卖出表现绝对差的资产。\n横截面动量：关注资产之间的相对表现。例如，在一个股票组合中，买入过去一段时间表现最好的几支，同时卖出表现最差的几支，形成一个“做多强势、做空弱势”的组合。\n基于这个策略架构，我将用 backtrader 测试动量策略在币圈的表现情况。\n策略定义 # 首先选择交易的池子，我将选择市值前 10 的加密货币作为交易标的，不包括 USDT 和其他的包装代币。\n交易逻辑采用横截面动量思路，选择每周内表现好的币种做多，同时做空表现差的币种，比例是五五开。这其实是实现了对冲，算是个中性策略，要保证多仓和空仓的价值相等。\n还有，即使每周的多空品种没有变化，也会再平衡，实现每个品种的风险暴露相同。\n因为没找到按日期查询前十市值数字货币的方法，我就按当前时间固定了 10 个币种。这里其实是引入了未来信息了。\n如果查看 CoinMarketcap 的历史快照页面，历史上的前 10 市值的品种变化不算太大。\n回测代码 # 策略代码实现起来非常简单，如下是策略的核心交易逻辑部分。\nimport backtrader as bt import backtrader.indicators as btind class MomentumStrategy(bt.Strategy): def __init__(self): self.pchgs = {} for data in self.datas: self.pchgs[data] = bt.ind.PercentChange(data, period=1) self.count = len(self.datas) self.cut_pos = int(self.count / 2) def next(self): sorted_datas = sorted( self.pchgs.keys(), key=lambda k: self.pchgs[k][0], ) total_value = self.broker.getvalue() target_value = total_value / self.count for data in sorted_datas[:self.cut_pos]: self.order_target_value(data, -target_value) for data in sorted_datas[-self.cut_pos:]: self.order_target_value(data, target_value) 指标是通过 backtrader 的 PercentChange 计算，period 设置为1。这样在我们加载周线数据到系统就行了。\nself.pchgs = {} for data in self.datas: self.pchgs[data] = bt.ind.PercentChange(data, period=1) 因为这是一个组合策略，有多个标的，指标是通过字典，键是 backtrader 的 dataFeed 对象，代表的具体的某个品种。\n在 next 策略逻辑方法中，按每周变化对各个标的排序。\nsorted_datas = sorted( self.pchgs.keys(), key=lambda k: self.pchgs[k][0], ) 接着就是每周定期按当前的持仓价值，在在平衡风险暴露，统一风险暴露。\ntotal_value = self.broker.getvalue() target_value = total_value / self.count for data in sorted_datas[:self.cut_pos]: self.order_target_value(data, -target_value) for data in sorted_datas[-self.cut_pos:]: self.order_target_value(data, target_value) 到这里，就核心的策略部分，每个周期按当前组合总价值对组合进行平衡。\n接下来是主函数部分。\nsymbols = [ \u0026#34;BTC/USDT\u0026#34;, \u0026#34;ETH/USDT\u0026#34;, \u0026#34;XRP/USDT\u0026#34;, \u0026#34;BNB/USDT\u0026#34;, \u0026#34;SOL/USDT\u0026#34;, \u0026#34;TRX/USDT\u0026#34;, \u0026#34;DOGE/USDT\u0026#34;, \u0026#34;ADA/USDT\u0026#34;, \u0026#34;BCH/USDT\u0026#34;, \u0026#34;LINK/USDT\u0026#34;, ] cerebro = bt.Cerebro(stdstats=False) # 只绘制 broker 资金和价值变化 cerebro.addobserver(bt.observers.Broker) cerebro.broker.setcash(1e8) cerebro.broker.setcommission(commission=0.0005) cerebro.broker.set_slippage_perc(0.0001) cerebro.addanalyzer(bt.analyzers.drawdown.DrawDown, _name=\u0026#34;drawdown\u0026#34;) if len(symbols) % 2 != 0: raise ValueError(f\u0026#34;标的个数是{len(symbols)}, 要保证能匹配成对\u0026#34;) 选择了市值排名前十的标的，设置初始资金为1e8，费率为万五，滑点为万一，还加入了回撤 Drawdown 分析器。\n如下加载数据和策略并运行回测代码。\nfor symbol in symbols: df = download( symbol, start_date=\u0026#34;2020-01-01\u0026#34;, end_date=\u0026#34;2025-11-30\u0026#34;, interval=\u0026#34;1w\u0026#34; ) data = bt.feeds.PandasData( dataname=df, name=symbol, ) data.plotinfo.plot = False cerebro.adddata(data) cerebro.addstrategy(MomentumStrategy) print(f\u0026#34;初始持仓价值：{cerebro.broker.getvalue()}\u0026#34;) strats = cerebro.run() print(f\u0026#34;最终持仓价值：{cerebro.broker.getvalue()}\u0026#34;) max_drawdown = strats[0].analyzers.getbyname(\u0026#34;drawdown\u0026#34;).get_analysis()[\u0026#39;max\u0026#39;][\u0026#39;drawdown\u0026#39;] print(f\u0026#34;最大回撤：{max_drawdown}\u0026#34;) cerebro.plot() 运行结果：\n初始持仓价值：100000000.0 最终持仓价值：605325844.8474668 最大回撤：17.627636120733097 绘图结果：\n看起来还不错，回撤 17% 也不是很大。不过这里用的周线数据，更细粒度的回撤没有被回测出来。\n还有这里没有风险控制，可以用日线或小时线回测看看，这里应该还有优化空间。\n数据下载函数 download 是我通过 ccxt 封装的函数，这里不暂时出来了。完整代码请查看地址 momentum.py。\n还有一点注意，加入的多个标的数据要保证日期对齐，否则会导致回测结果错乱。\n最后 # 本文是基于动量这个思路的策略回测，如果从周线上看，这个动量效应还是比较明显的，或许这和高杠杆有很大关系吧。\n如果你还没有账户，可以通过以下链接注册。这些是我在交易和开发中常用的交易所：\nOKX：https://www.bjwebptyiou.com/join/18465372 Bybit：https://www.bybit.com/invite?ref=EPMPL Binance：https://accounts.maxweb.cab/register?ref=52142925 使用邀请链接注册可能会根据各平台的政策获得一定的推荐权益。\n","date":"2025-12-02","externalUrl":null,"permalink":"/posts/2025-12-02-moment-strategy/","section":"文章","summary":"在金融市场，有一种被称为 “动量” 的现象。其核心理念很简单：如果一个资产（如股票）的价格正在上涨，那么它短期内更有可能继续上涨；同样，如果一个资产的价格正在下跌，它也更有可能继续下跌。这就像惯性一样。\n","title":"用 backtrader 回测动量策略","type":"posts"},{"content":"上篇文章测试了在加密货币市场上定期定额定投的表现，以求能囤到足够价码的加密货币。\n本文我们继续这个话题，尝试基于技术指标定投。\n在币圈你会看到一些 KOL 喜欢推崇 DCA，定期定额投入不要考虑时机。于是，我就想实际测试，是不是择时真的无效，无法战胜定期定额的 DCA 呢？\n注意：本文仅为个人随笔与思考，不构成任何投资建议。市场有风险，投资需谨慎，请务必独立研究。\n如果你对本文中的回测代码感兴趣，请访问代码仓库：dcastrategy_backtest。希望没有错误。\n提前说明 # 定期定额方案最大优势是，能保证在固定时间内能投入足够金额购买加密货币，最大化资金利用。但这种方式在成本价上没有任何优化。\n如果加入择时能力，是否降低囤币的成本价呢？\n指标定投是带有择时的能力的，但指标定投时机不确定，无法明确特定时间范围得的投资次数，这是指标定投的最大缺点。\n为便于和定期定额比较，我们只比较买入的成本价。最基本的目标，成本价要能战胜定期定额。\n还有，为了尽量保证指标定投能买入到足够金额的币，记录下整个定投周期的定投次数，反推每期定投的金额，尽可能防止每期投入过多或太少，导致资金不足或剩余太多现金。\n如你的工资是每月3000 美金，每月有 1000 美金可用于投资加密货币，那五年的总投入总额就是 6 万美金。\n假设回测过去五年，满足定投的次数有 600 次，那每笔定投的金额就控制在100美金即可。\n如果出现连续几个月的时间没有机会，每月积累的资金可用来套利，等待时机进行定投。\n当然，有可能刚开始定投就遇到了连续出现定投机会，这时候可能出现预算不足的情况，这时要抉择是否预支接下来几个月的资金满完成定投计划了。\n本文将尝试分析基于指标定投的策略表现，主要用趋势和震荡指标进行定投。\n品种和时间线 # 为了和上文的定期定额进行比较，回测品种依然是 BTC，而回测时间还是从 2020-01-01 到 2024-12-01。\n震荡指标定投 # 首先是基于震荡指标定投，选择两个最常用的震荡指标： RSI 和布林带。定投规则是，只有在处于超跌状态才进行定投。\n这两个指标超跌状态的定义：\n对于 RSI，RSI \u0026lt; 30 表示当前处于超跌状态； 对于布林带，当前价格位于布林带下轨表示当前处于超跌状态； 如下是 RSI 的定投表现。\n周期 成本 定投次数 每周 20498.27 5 每日 17984.03 59 每四时 21246.42 464 每小时 22992.13 1980 如下是布林带的定投表现。\n周期 成本 定投次数 每周 21168.85 5 每日 21792.87 83 每四时 23619.99 669 每小时 24136.93 2682 上篇文章测试定期定额的成本价，普遍在 2.3 万附近，这里通过指标定投的大部分成本价都在 2.2 万以下，基本都能战胜定期定额。\n总体而言，基于日线 RSI 超跌状态的定投次数最合理，5年时间一共有 59 次定投机会，同时成本价 17984 也是最低的，相较于定期定额的 2.3 万成本价，这个优势还是更强的。\n看起来基于超跌指标定投是个不错的方案。\n不过在测试 ETH 的定投时，我发现在 2020-01-01 到 2024-12-31 期间，ETH 的定期定额（成本价 800 多）优于指标定投（成本价是 900 多）的，这里的重要原因就是 ETH 在2020 年的涨幅太大，从 120 附近暴涨涨到了 900 多，拉高了超跌定投的成本价。\n不过那种暴涨毕竟是少数，如果去掉这一年，从 2021-01-01 开始，超跌定投明显优于定期定额。\n如果你想自己测试，对应的代码地址：RSI 定投，布林带定投。\n趋势指标定投 # 虽然说在上面的测试中，超跌指标优于定期定额。但如果仔细观察图表会发现，经常有在超跌状态出现依然会持续很长一段时间的下跌。\n能不能等出现趋势反转时，再入场定投呢？从而防止抄底抄在半山腰呢？\n我们验证下这个想法吧。\n如果定义可能的趋势反转状态呢？我想到的就是长期下跌后出现趋势反转的信号。\n首先，如何定义长期下跌？\n简单点，就是短期均线长期位于长期均线下方。\n如何定义反转呢？\n就是短期均线和长期均线发生金叉。\n当然你可以选择其他定义，我这里就测试这一个就行了。这逆势反转的定义，参数较多，相对比超跌复杂，你的选择对表现影响应该是挺大的。\n我长期观察，短期10 EMA 均线和长期 20 EMA 均线对趋势判断效果不错，这里就用这个参数来测试。\n定投时机的定义是，如果短期 10 EMA 位于长期 20 EMA 超过 20 个时间周期后出现金叉，进行定投。\n如下是测试结果：\n周期 成本 定投次数 每周 22932.91 1 每日 19812.84 15 每四时 21617.82 94 每小时 23392.95 414 看起来效果还不错，总体还是日线定投的效果最好，依然是大部分周期都能战胜定期定额的 2.3 万的成本价。\n不过相较于直接在超跌就定投并没有什么优势。这或许是因为 BTC 长期上涨的趋势比较好，超跌信号的有效性比较强。\n如果在 ETH 上测试，等反转信号出现在进行定投的表现会优于超跌状态就定投的表现，应该是因为 ETH 最近几年一直处于大的震荡区间。\n我这里没有做任何参数优化，就是长期观察，拍脑袋得到的参数。如果你有兴趣，可以考虑做一些参数优化，或是基于这个大思路定义你的长期下跌会趋势反转量化方式。\n如果你想自己测试，对应的代码地址：EMA 回测。\n最后 # 本文回测了采用指标择时定投的思路，总体表示还是优于无脑的定期定额，不过这里如果要保证足额投入，还要从回测的定投次数去反推每期的定投金额，防止过于自信，早早把手里的子弹耗尽。\n最后，希望本文能给你带来一些不一样的思考。\n","date":"2025-11-19","externalUrl":null,"permalink":"/posts/2025-11-19-dca-stratey-in-cryptocurrency-part3/","section":"文章","summary":"上篇文章测试了在加密货币市场上定期定额定投的表现，以求能囤到足够价码的加密货币。\n本文我们继续这个话题，尝试基于技术指标定投。\n在币圈你会看到一些 KOL 喜欢推崇 DCA，定期定额投入不要考虑时机。于是，我就想实际测试，是不是择时真的无效，无法战胜定期定额的 DCA 呢？\n","title":"探索定投囤币方案之基于指标择时定投","type":"posts"},{"content":"今天这篇博文想谈点交易感悟，该如何对待资讯。\n做交易久了，会慢慢明白一个事实：\n你看到的资讯，并不等于你能做出正确决策的依据。\n或许资讯不是完全没用，但也绝对不是普通人能依赖的“武器”。\n刚开始做交易时，我也以为资讯很重要 # 那时候我以为只要比别人多看一点、快看一点，就能多抓住一点机会。\n公司出利好了，立马冲进去； 某大V喊单了，马上跟着买； 政策公布前后，拼命研究各类解读…… 但慢慢就发现： 绝大多数时候，市场早就提前反应，等你反应过来，节奏已经结束了。\n你追进去的那一刻，其实已经是“别人出货”的时候。\n资讯真正的难点，不是“真假”，而是“能不能用” # 我们要分清楚资讯的几个类型：\n有些资讯，是真实的，但已经被提前消化 比如非农、CPI、政策指引，有可能早已体现在价格中。\n有些资讯，是带节奏的内容 KOL喊单、自媒体热议、神秘“内线消息”……它们的目的是吸引你进场，不是帮你做决策。\n有些资讯，是不确定性事件，解释可以正着讲也可以反着讲 比如美联储加息，有人说利空经济，有人说利多美元强势，众说纷纭，根本没法用来落地操作。\n最关键的问题是：你作为普通交易者，是否有能力正确理解并快速利用这些资讯？\n如果没有，看得越多，反而容易被情绪影响，越交易越不理性。\n有些 \u0026ldquo;局\u0026rdquo; 不是我们能参与的 # 这不是阴谋论，而是市场机制：\n有资金有资源的人，可能能提前知道消息； 有影响力的人，能通过制造舆论影响市场； 有系统有模型的人，能不看内容，只看“反应数据”做交易。 而普通人，往往是最后看到资讯、却第一个情绪反应的人。\n这就是“不是做局之人，就别盲目参与这个局”的含义： 不是我们不能看资讯，而是要知道我们在哪个位置，看这些有什么风险。\n普通人真正的优势是什么？ # 不是资讯量，而是：\n规则感：用看得懂、能执行的逻辑来做交易； 纪律性：不因为外界干扰而轻易更改自己的节奏； 长期主义：不是抓住一波就暴富，而是持续跑赢情绪波动的人群。 我见过不少稳定盈利的人，他们很少看资讯，更关注的是：\n技术面形态是否走出来了； 策略信号有没有触发； 当前仓位是否控制合理； 盈亏比、胜率是否匹配预期。 他们不是“闭目塞听”，而是“心里有一把尺子”。\n靠资讯赚钱的人真的存在吗？ # 确实存在一些“看起来靠资讯赚钱”的人，但我们往往只看到了表面：\n有人是做事件驱动交易的，背后其实有完整的规则体系； 有人靠的是市场对消息的行为反应，而不是消息内容本身； 有人有资源或速度优势，比如机构级的新闻推送或前置信息流； 有人其实只是“马后炮复盘”，借机宣传，亏的时候你看不到。 所以问题不在于“有没有人能靠资讯赚钱”，而是：\n大多数人没有那样的系统、速度和经验，却照搬这种方式，很容易走火入魔。\n如果你没有一个成熟的、规则化的资讯反应机制，仅靠看到一条新闻或一个利好就临时改交易决策，那很可能是被市场节奏牵着走。\n资讯可用，但它不是你策略的核心支点。 普通交易者真正要问的是：\n“这个资讯我理解了吗？我真的能用它制定交易吗？我能重复地用它吗？”\n如果不能，可能你看到的，不是交易机会，而是情绪诱饵。\n资讯到底该不该看？ # 说到底，资讯是工具，关键在于“怎么用”和“什么时候用”。\n对于中长线的仓位管理、宏观趋势判断，资讯当然有参考价值。 比如全球货币政策、经济周期、行业趋势等，确实能帮你构建认知框架。\n但对大多数做短线、趋势或日内交易的普通人来说，看资讯往往会引发更多不必要的干扰，而不是提升决策质量。\n不是不能看，而是要明确：\n你看资讯，是为了强化系统，还是动摇了系统？\n你该选择哪条路？ # 如果你发现自己频繁因为资讯临时改策略、追涨杀跌、情绪起伏—— 那可能不是策略不行，而是你的注意力和节奏，被别人的信息劫持了。\n在这个节奏极快、信息过载的市场里， 真正的交易能力，来自筛选、取舍、沉得住气。\n最后，送你一句我常提醒自己的话：\n资讯是风，不是舵。你要做的，是造好自己的船。\n","date":"2025-07-10","externalUrl":null,"permalink":"/posts/2025-07-10-news-is-not-a-trading-strategy/","section":"文章","summary":"今天这篇博文想谈点交易感悟，该如何对待资讯。\n做交易久了，会慢慢明白一个事实：\n你看到的资讯，并不等于你能做出正确决策的依据。\n或许资讯不是完全没用，但也绝对不是普通人能依赖的“武器”。\n","title":"交易这条路上，资讯是工具，但不是答案","type":"posts"},{"content":"本文将一步步完成 Backtrader 的安装，并通过两个简单策略——买入持有与均线交叉策略，带你快速熟悉 Backtrader 的结构与用法。\n快速安装 # Backtrader 的安装过程非常简单，即使你是刚接触 Python，也能快速搞定。\n打开你的命令终端（比如 Windows 的 CMD 或 Mac 的终端），敲入这行代码：\npip install backtrader 输入 \u0026ldquo;Enter\u0026rdquo; 确认就能很快安装就完成。\n简单例子 # 先通过一个最小化的可运行示例，模拟用历史数据加载苹果股票（AAPL），并通过 Backtrader 运行并画图。\n这个例子不带任何交易策略，仅用于感受框架整体流程。\nimport backtrader as bt from datetime import datetime import yfinance as yf import pandas as pd # 使用 yfinance 下载苹果股票数据并转换为 Pandas DataFrame raw_data = yf.download(\u0026#39;AAPL\u0026#39;, start=\u0026#39;2020-01-01\u0026#39;, end=\u0026#39;2021-01-01\u0026#39;, multi_level_index=False) raw_data.reset_index(inplace=True) # 转换为 Backtrader 可识别的 Pandas 数据源 apple_data = bt.feeds.PandasData(dataname=raw_data) cerebro = bt.Cerebro() cerebro.broker.setcash(1e8) cerebro.adddata(apple_data) print(f\u0026#34;Initial portfolio value: {cerebro.broker.getcash()}\u0026#34;) cerebro.run() print(f\u0026#34;Final portfolio value: {cerebro.broker.getcash()}\u0026#34;) cerebro.plot() 这将生成一个图表，显示价格走势与策略执行情况，能让你第一时间看到回测结果。\n图表上的资产净值从开始就没有变化，因为这个例子中没有添加任何的策略。\n简单介绍下这段代码吧。\n第一步，创建回测引擎并设置初始资金： cerebro = bt.Cerebro() cerebro.broker.setcash(1e8) cerebro 是整个大脑，负责管理策略逻辑、数据源、资金、回测过程等。\n第二步，加载数据，以 apple 股票数据为例： apple_data = bt.feeds.PandasData(dataname=raw_data) cerebro.adddata(apple_data) 这里使用的是 yfinance 下载的 DataFrame 数据，并通过 PandasData 转为 Backtrader 所能识别的数据格式。\n第三步，执行回测： cerebro.run() Backtrader 会从头到尾执行策略逻辑，即使此时你没有写具体的交易逻辑，它也会完整遍历数据。\n最后一步画图展示： cerebro.plot() 这个例子没有写一行策略代码就能跑起来，很适合初学者快速上手。\n买入持有策略 # 开始用 backtrader 回测一个最简单的策略：买入持有。\n买入并持有是一个朴素的投资理念，很多人会说：“如果一直持有不卖，现在早赚翻了！”。尝试用 backtrader 的代码实现这个简单的投资逻辑。\nclass BuyHold(bt.Strategy): def next(self): if not self.position: self.buy() cerebro = bt.Cerebro() cerebro.addstrategy(BuyHold) cerebro.adddata(apple_data) cerebro.broker.setcash(100000) print(f\u0026#34;Initial portfolio value: {cerebro.broker.getcash()}\u0026#34;) cerebro.run() print(f\u0026#34;Final portfolio value: {cerebro.broker.getcash()}\u0026#34;) cerebro.plot() 输出：\n执行策略，就可以清楚看到，策略在第一个交易日买入股票后就再也没有新的动作，账户资金和股价变化基本保持一致。\n理解三个点：\n第一，策略是通过 cerebro.addstrategy() 这句代码加载进回测引擎的，这就像是把你的交易逻辑交给“大脑”去执行；\n第二，策略本身的逻辑是通过继承 bt.Strategy 并实现 next() 方法来定义的。\nBacktrader 每处理一根 K 线数据，都会自动调用策略的 next() 方法。这个方法就像策略的“心跳”，交易逻辑都在这里。\n第三，策略实现部分，通过 self.position 判断当前是否持仓，是最基础也最重要的条件判断语句之一。\n当你写下 if not self.position: self.buy() 时，已经完成了一个完整的量化判断过程：检查当前是否有仓位，然后在满足条件时买入。\n这个代码整体既直观，又具备强大的扩展能力。接下来，再尝试实现一个更加复杂的策略吧！\n均线交叉策略 # 在交易策略里，“金叉买入，死叉卖出”绝对是老生常谈。但真实情况如何呢？\n通过 Backtrader 亲自测试下吧。\n如下是均线交叉策略的实现：\nclass SMACross(bt.Strategy): def __init__(self): # 初始化时定义两个均线指标：快速和慢速 self.fast_ma = bt.indicators.SMA(period=10) self.slow_ma = bt.indicators.SMA(period=30) def next(self): # 每根K线都会调用此方法，执行一次策略判断逻辑 if not self.position: # 没持仓时，若发生金叉（快线由下向上穿过慢线），则买入 if self.fast_ma[0] \u0026gt; self.slow_ma[0] and self.fast_ma[-1] \u0026lt;= self.slow_ma[-1]: self.buy() else: # 已持仓时，若发生死叉（快线由上向下穿过慢线），则卖出 if self.fast_ma[0] \u0026lt; self.slow_ma[0] and self.fast_ma[-1] \u0026gt;= self.slow_ma[-1]: self.sell() 输出：\n直观感受到“金叉买入，死叉卖出”的效果到底如何，收益和亏损都会清晰地展现了出来。\n这个策略用 backtrader 内置技术指标 bt.indicators.SMA 计算移动平均线。\n在 next() 方法中，通过判断当前与前一根K线的均线关系，识别出“金叉”与“死叉”信号。\n如果想简化 if 金叉死叉的判断条件，还可以通过 bt.ind 的 CrossUp 和 CrossDown 计算是否发生了交叉。\nself.crossup = bt.ind.CrossUp(self.fast_ma, self.slow_ma) self.crossdown = bt.ind.CrossDown(self.fast_ma, self.slow_ma) 买入条件就是：\nself.crossup[0] 卖出条件就是：\nself.crossdown[0] Backtrader 指标系统简化了策略实现过程，提高了可读性稳定性。\n现在只要将 cerebro.addstrategy 中 BuyHold 策略修改为 SMACross 即可用新策略回测了。\n六、简单介绍分析指标 # 在使用 Backtrader 时，你还可以轻松添加各种分析指标，比如年化收益率、夏普比率、最大回撤等。这些指标能帮你快速、清楚地评估策略表现，从而更科学地判断策略好坏。\ncerebro.addanalyzer(bt.analyzers.SharpeRatio, _name=\u0026#39;sharpe\u0026#39;) results = cerebro.run() sharpe_ratio = results[0].analyzers.sharpe.get_analysis() print(\u0026#34;夏普比率：\u0026#34;, sharpe_ratio) Backtrader 内置了大量分析工具可以直接调用。例如：\nDrawDown：查看回撤表现，衡量风险暴露 Returns：计算每日或周期收益率 TradeAnalyzer：输出每一笔交易的盈亏、胜率等细节 SQN：系统质量指数（System Quality Number），衡量策略整体表现 TimeReturn：用于时间序列的收益计算 这些分析器都可以像 Sharpe 一样通过 addanalyzer() 方法添加，并通过 get_analysis() 获取结果。\n有了这些分析指标，你的交易评估会更加全面和精准。使用得当，它们能帮助你对策略进行多维度评价，辅助决策与优化。\n总结 # 本文从 Backtrader 的安装开始，带你跑通了最基础的回测流程。\n从加载数据、创建回测引擎，到实现两个基础策略（买入持有与均线交叉），基本过了下 Backtrader 基本结构和用法。\n此外，还初步接触了如何使用分析器评估策略表现。\n希望这些内容能助你对 Backtrader 有了初步的认识，建立起对量化回测的整体感知。\n","date":"2025-07-05","externalUrl":null,"permalink":"/posts/2025-07-05-backtrader-tutorial-p2/","section":"文章","summary":"本文将一步步完成 Backtrader 的安装，并通过两个简单策略——买入持有与均线交叉策略，带你快速熟悉 Backtrader 的结构与用法。\n快速安装 # Backtrader 的安装过程非常简单，即使你是刚接触 Python，也能快速搞定。\n","title":"Backtrader 教程二：安装与快速开始","type":"posts"},{"content":"今天想跟大家聊一个量化交易中非常实用的工具——Backtrader。\n如果你对量化交易感兴趣，可能已经听说过策略回测。简单说，就是用历史数据去验证一个交易策略有没有效。在Python里，其实有不少回测框架可以用，比如backtesting.py、vectorbt，还有已经不更新的 zipline。\n但今天，我要重点介绍的是——Backtrader。\n🤔 为什么选Backtrader？ # 因为构建一个稳健的交易系统，光有策略还不够，还需要有清晰的架构、可扩展的功能，以及模块化的设计。而Backtrader，就是一个以策略回测为核心，同时帮你搭建交易系统骨架的Python框架。\n它不只是回测工具，更是一个轻量级的“交易系统骨架”。\n🔍 Backtrader是什么？ # Backtrader是一个用Python写的量化回测框架，专门为个人交易者和研究者设计。它支持策略开发、历史回测、可视化分析，甚至还能接入实盘交易。\n更重要的是，它采用面向对象的设计，把策略、数据、订单、资金账户这些模块分得清清楚楚，你不用把所有代码都塞在一个脚本里，而是像搭积木一样，有条不紊地搭建系统。\n🧱 核心架构：五大模块 # Backtrader的架构非常清晰，主要由五个模块组成：\n策略逻辑（Strategy）\n你把买卖信号的判断写在这里，框架自动帮你执行订单，你不用操心具体怎么成交。\n数据输入（DataFeed）\n支持从CSV、Pandas、甚至实时API导入数据，不管是日线、分钟线，还是多个股票，都能一起回测。\n交易与资金管理（Broker）\n模拟真实市场的成交，支持市价单、限价单、滑点、佣金计算，自动更新你的账户余额和持仓。\n绩效分析（Analyzer）\n回测结束后，自动计算收益率、最大回撤、夏普比率等指标，帮你全面评估策略表现。\n图形展示（Observer）\n自动画出买卖点、资金曲线、技术指标，一眼看懂策略行为。\n这五个模块各司其职，而你只需要专注在策略逻辑上。\n🔄 一张图看懂回测流程 # 从数据加载 → 策略判断 → 订单生成 → 成交撮合 → 绩效分析 → 图形输出，整个过程都由一个叫Cerebro的引擎统一调度。你只需要写策略，其它交给Backtrader。\n📊 和其它框架比，Backtrader强在哪？ # ✅ 完全本地运行，不依赖网络，数据更安全\n✅ 支持多策略并行运行，方便组合对比\n✅ 支持多股票、多周期数据一起分析\n✅ 自带可视化，不用另外画图\n✅ 结构清晰，组件解耦，易扩展维护\n💡 和手写回测的对比 # 很多人一开始学量化，会用Pandas写循环来回测，比如：\nfor i in range(1, len(data)): if data[\u0026#39;close\u0026#39;][i] \u0026gt; data[\u0026#39;close\u0026#39;][i-1] and position == 0: # 买入... elif ... 这样写虽然直接，但策略一复杂，代码就变得又乱又难维护。\n而用Backtrader，同样的策略可以这样写：\nclass MyStrategy(bt.Strategy): def next(self): if self.data.close[0] \u0026gt; self.data.close[-1]: self.buy() elif self.data.close[0] \u0026lt; self.data.close[-1]: self.sell() 清晰、干净，所有底层操作都被封装好了。\n🎯 总结 # 我喜欢Backtrader，就是因为它让我更专注于策略本身，而不是重复造轮子。\n它干净、模块化、易于扩展，不管你是刚入门，还是想搭建自己的交易系统，Backtrader都是一个非常理想的起点。\n下一期，我会带大家安装Backtrader，并实现一个最简单的“买入持有”策略。如果你对量化交易感兴趣，别忘了关注我，我们下期见！\n","date":"2025-07-03","externalUrl":null,"permalink":"/posts/2025-07-03-backtrader-tutorial-p1/","section":"文章","summary":"今天想跟大家聊一个量化交易中非常实用的工具——Backtrader。\n如果你对量化交易感兴趣，可能已经听说过策略回测。简单说，就是用历史数据去验证一个交易策略有没有效。在Python里，其实有不少回测框架可以用，比如backtesting.py、vectorbt，还有已经不更新的 zipline。\n","title":"Backtrader 教程一：为什么选择 Backtrader？","type":"posts"},{"content":"本文尝试分析不止盈的定投策略，目标是寻找一个更优囤币的方案，我将先从简单的定期定额开始入手。\n注意：本文仅为个人观点分享，记录研究过程，不构成任何投资建议。投资有风险，入市需谨慎，请读者自行研究并独立承担相应后果。\n如果对回测代码有兴趣，在公众号回复 \u0026ldquo;dcastrategy-bt-p1\u0026rdquo; 获取代码地址，回测采用 backtrader 实现，暂只适用于 7x24 小时无休市场，没有针对如股票市场交易日历的处理。简单处理的话，可以将它们的行情做填充补齐。\n定投目标 # 什么是更优囤币的方案？我认为，肯定是能以更低价格且足额数量买到你心仪加密币的方案。\n什么是更优的价格？\n这个很好评价，即总持仓的成本越低越好，持仓成本的计算公式如下：\n$$持仓成本 = \\dfrac{总投入金额}{总持仓数量}$$是不是持仓越低越好呢？ 当然不是，我还要保证买入的金额是足够的。\n那什么是足够的金额呢？\n这可以基于你的工作薪资制定定投金额，基于定投周期和推算出的目标金额，就是你计划投入的足额金额。\n举个简单的例子\n假设你每个月有 1000 USD 的额度可用于定投，如果目标是五年每月定投 1000 USD 到 BTC，则定投的目标总金额就是 60k USD。\n如果你的定投计划不能完全把你计划的金额全部投进去，那某种意义上就是资金的浪费。\n到这里，目标就明确了，我在一定的时间内，花完计划投入的所有金额，且买入的成本要足够低。\n那么，为了完成这个目标，我有哪些策略可以使用呢？\n定期定额 # 首先，讨论定期定额的定投策略，它是否满足我们对定投的目标。\n这个方案与定投目标算是正好契合，因为它就是定期购买固定额度。这能保证计划的金额在规定时间内全部兑换为我的目标数字币。\n按前面的例子，你每个月计划 1000 USD 用于定投 BTC，定投五年，投入金额为 60k USD。\n按这个方案，假设每月 1 号定投，我回测了过去五年（2020-01-01 到 2024-12-31）的表现。\n如下回测得到表现数据：\n投入金额: 60000 USD； 囤币数量: 约 2.60 BTC； 持仓成本: 23100.60； 这个持仓成本在 23000 附近，还是比较感人的，最终囤了 2.59 BTC。\n现在就将这个作为基准去测试其他定投方案吧。\n除了按月这个定投周期，还有选择其他周期，如每周、每双周、每日等，可以回测这些不同的周期，看看是否能带来一定的优势。\n我简单测试了每日和每周的表现，将它们的表现数据与每月定投合并，如下所示。\n定投周期 单次金额 总投入金额 囤币数量 持仓成本 每日 32.86 60032.86 2.58 23289.12 每周(星期一) 230.77 60230.77 2.58 23353.10 每双周(星期一) 461.54 60461.54 2.61 23135.35 每月(1号) 1000.00 60000.00 2.60 23100.60 持仓成本最低的是 \u0026ldquo;每月（1号）\u0026rdquo; 的定投方案。总投入金额有点误差是因为根据总金额 60k 推算不同投资频率推算单次投入金额产生的小误差。\n对比观察下来，并没有看到那个定投周期有明显的优势。\n星期择时效应 # 在探索定投策略时，或许我们很多人都想过是否有某星期几定投有明显的优势。在长期定投中，不同的星期几进行买入，可能因为市场情绪、流动性、宏观数据发布时间等因素，造成最终买入成本的差异。\n为了验证这个效应是否在定投中能产生显著影响，可以将每周定投进一步细分，比如：\n每周一定投 每周三定投 每周五定投 …… 接下来是测试是否有星期择时效应，通过周定投不同星期几的表现，看看是否能找到更具优势的买入节奏。\n还是从 2020-01-01 到 2024-12-31，定投 5 年，总投入 60230，单次投入 230.77，数据表格如何如下：\n定投周期 囤币数量 持仓成本 星期一 2.58 23353.10 星期二 2.57 23464.77 星期三 2.59 23291.68 星期四 2.60 23205.95 星期五 2.59 23256.04 星期六 2.59 23250.07 星期日 2.60 23204.44 柱状体如下：\n周三和周日的持仓成本最低，不过整体差距很小，囤币数量也只有 0.01-0.03 的差别。\n有兴趣，还可以测试下最近两年的影响，BTC 是最近两年开始与美国股市的关联度越来越高。\n月份日期择时测试 # 测试按周定投后，再顺手测试下每月不同日期定投的表现，时间范围 2020-01-01 到 2024-12-31，总投入 60k USD，单次投入 1000 USD。\n如下是回测数据：\n定投周期 囤币数量 持仓成本 1号 2.60 23100.60 2号 2.61 22975.42 3号 2.61 22946.59 4号 2.60 23066.85 5号 2.59 23183.45 6号 2.56 23476.48 7号 2.55 23523.63 8号 2.55 23498.30 9号 2.57 23365.81 10号 2.58 23272.63 11号 2.59 23152.50 12号 2.66 22562.61 13号 2.62 22911.26 14号 2.61 23017.94 15号 2.61 23014.27 16号 2.61 22969.79 17号 2.59 23149.35 18号 2.59 23164.28 19号 2.57 23310.58 20号 2.57 23320.30 21号 2.59 23127.20 22号 2.59 23193.94 23号 2.56 23448.17 24号 2.56 23463.80 25号 2.56 23419.53 26号 2.56 23448.13 27号 2.54 23596.07 28号 2.53 23722.34 柱状图如下：\n总体差异不是很大，虽然有 12-16 号的持仓成本相对较低，但我感觉和2020年3月12号的大跌行情关系较大，不具备代表性。\n如下是改为从 2021-01-01 到 2024-12-31 测试的图表：\n换了个时间范围，这个优势就基本没了。\n总结 # 本月先明确了定投的目标，接着粗略测试了定期定额的定投方案。暂时来看，无论是按日按周还是按月的表现差不大，没有那个方案有带来明显优势。\n我现在只是测试了 BTC，这个币的趋势这几年基本是一直向上。其他如 ETH 这种几年没怎么涨的币种也可以测试下看看效果。\n后续的计划，我会以这个定期定额为基础，引入其他策略，看看是否有办法保证足额投入的同时，降低持仓成本。\n记录我的粗浅思考过程，感谢阅读。\n","date":"2025-05-27","externalUrl":null,"permalink":"/posts/2025-05-30-dca-stratey-in-cryptocurrency-part2/","section":"文章","summary":"本文尝试分析不止盈的定投策略，目标是寻找一个更优囤币的方案，我将先从简单的定期定额开始入手。\n注意：本文仅为个人观点分享，记录研究过程，不构成任何投资建议。投资有风险，入市需谨慎，请读者自行研究并独立承担相应后果。\n","title":"探索定投囤币方案之定期定额","type":"posts"},{"content":"最近在研究定投策略在数字货币上的表现，计划实际回测下不同的定投策略的表现。\n本文尝试先整理定投的一些概念，梳理下我对定投的理解，就是随便瞎扯犊子。\n理清几个概念 先明确几个容易被混乱的概念，分别是 DCA、定投和马丁格尔。这几个名词是我看几个交易机器人时产生的迷惑，它们的命名并不统一。\n定投常见的理解就是定期定额投资，至少是有定期的概念，而定投金额可能有动态调整的策略，如位于均线下方多投一些。\n马丁格尔策略是一种加倍下注的交易方法，每当亏损时就加倍下单，期望最终一次盈利能覆盖所有前亏并获得利润。\nDCA 的英文全名是 Dollar Cost Averaging，平均成本法，中文通常解释为定投，就是定期定额投入，但也有变种，亏损倍投，分批买入实现降低成本。\n不同交易所的命名 一部分加密货币交易所，DCA 就是定投，即定期定额的交易策略。而有些平台，定投和 DCA 代表的是不同的策略，DCA 实际是马丁格尔策略。\n这几个概念不同交易所的命名上有差异，了解本质就行，没必要纠结命名差异。无论是马丁格尔还是定期定额策略都是在分批买入，平均成本。\n除了简单的定投，像 3Commas 这种交易机器人的平台，还提供了非常复杂的定投策略（DCA），如 RSI 超卖定投，MA 定投、下跌百分比定投等。除此意外，还可以设置复杂的止盈策略。\n不啰嗦了。统一称为定投吧，都是分批买入，区别在于是否定期与是否定额，可统一分为四类，即定期定额、定期不定额、不定期定额和不定期不定额。\n止盈与不止盈 我觉得定投还有一个比较重要点：是否止盈，它对确定我的定投策略有什么影响？\n不带止盈定投： 即一直买、一直拿着不卖；\n带止盈的定投： 即涨到一定程度就卖出止盈；\n虽然看起来它们只是“卖”还是“不卖”的区别，但这两种方式背后的思维方式和目的完全不同。\n不止盈的定投，更像是“存钱”。\n就像你每个月往账户里存 100 美元，不管比特币是涨是跌，反正你长期看好它，打算放个五年、十年甚至更久。这种方式适合那些相信比特币长期会上涨、不太关心短期涨跌的人。省心、简单，而且拉长周期后通常能获得不错的回报。\n设有止盈点的定投，则更像是 \u0026ldquo;交易\u0026rdquo; 或 \u0026ldquo;套利\u0026rdquo;。\n你在价格低的时候定投，一旦价格涨到某个目标，比如赚了50%、翻倍了，就卖出止盈，把钱收回来。这种方式适合想更快看到收益，或者对风险更敏感、想把握市场波动机会的人。\n为什么谈它们的区别？ 我开始一直如何将两个策略放在一起比较，但发现单纯从收益、风险这些评价纬度比较它们并不合适。逐渐明白，没有必要把它们放一起比较。\n基于场景决定是否止盈 如果你的目标是长期持有、积累资产，那么不带止盈的定投就是在“持续囤币”，关注的是资产的长期增值，而不是短期波动。\n如果你的目标是提高收益、灵活调配资金，那么带止盈的定投其实就是一种策略性交易，通过设定止盈点锁定收益、提升资金利用率。\n不止盈的定投强调“时间复利”和“稳定积累”，适合长期信仰者；而带止盈的定投则更像是“低买高卖”的波段套利，更适合资金效率优先的交易者。\n如果没有强大的信仰，带有止盈后续更适合你。\n止盈后的资金如何利用 带有止盈的定投，还有一个比较重要点，要思考止盈后的资金该如何利用。\n我们可重启定投，但重启定投后的资金是无法全部利用的。\n多出来的资金如何处理呢？\n这些资金可配置到理财产品，获取低风险收益；\n亦或是配置其他策略，如我之前介绍的一些低风险策略如资金费率套利、期现套利。定投止盈时，可能就是牛市行情，这个时候这些低风险套利策略正好是高收益的时刻。\n当然，配置一些高风险的 CTA 策略，博取更大收益，也是不错的选择。\n总结 本文随便扯扯，只是谈谈个人想法，感觉阅读。\n本文只是个人思考，交易有风险，请自行研究，明确承担的风险，谢谢！\n","date":"2025-05-26","externalUrl":null,"permalink":"/posts/2025-05-26-dca-stratey-in-cryptocurrency/","section":"文章","summary":"最近在研究定投策略在数字货币上的表现，计划实际回测下不同的定投策略的表现。\n本文尝试先整理定投的一些概念，梳理下我对定投的理解，就是随便瞎扯犊子。\n","title":"对加密货币市场定投一些粗浅思考","type":"posts"},{"content":"我花了一个星期通读了 backtrader 的官方文档，順便还通过 AI 整理出了它的中文文档，我不想写 backtrader 的基础入门教程，还不如直接用 backtrader 逐步展开回测这一个策略，或许更加容易掌握它的使用。\n相较于其他回测框架，backtrader 的功能强大，除了支持单品种，还可以实现组合策略，扩展能力非常强大，如果说缺点，一是这个项目基本不维护了，不过不影响它的使用，还有就是因为它很强大，扩展性强，比其他的回测框架更不易于掌握。\n本文介绍如何使用 backtrader 回测双均线交易策略，将包含基本的进出场、风险控制，参数优化等，还有如何加载分钟数据尽可能模拟真实场景。我暂时先用双均线策略上手 backtrader，后续还会使用 backtrader 回测更多的策略。\n策略定义 # 选择品种\n暂定 BTC 作为回测品种，虽然这个策略在它上面的表现不算很好。 技术指标\nEMA，即指数移动均线，均线指标种类繁多，如 SMA、WMA 等； ATR，真实波动幅度，用于控制止损； 策略参数\nshort_period：短均线周期； long_period：长均线的周期 atr_period： 真实波动振幅的计算周期，一般固定为 14 即可。 入场条件\n做多：短均线上穿长均线。 做空：短均线下穿长均线。 出场条件\n平多：短均线下穿长均线。 平空：短均线上穿长均线。 风险控制\n为了防止出现过于大的止损金额，设置固定的止损位置并通过止损位置推算最大亏损金额。\n止损位置：当前价格 - n 倍 ATR； 最大亏损：每笔交易的最大亏损金额不超过账户的 2%，按次目标推算实际下单金额。 这是个非常简单的策略，只是相对于最简单版本的均线交叉策略，加上了风险控制。\n依赖库 # 安装依赖的 Python 库。\npip install backtrader yfinance 主流程 # 通过 backtrader 回测策略，在主函数要提前做一些设置，如设置费率、滑点、账户金额。此外还有加载数据源和策略。\nimport backtrader as bt import yfinance as yf # 策略逻辑，暂空 class MACrossStrategy(bt.Strategy): pass if __name__ == \u0026#34;__main__\u0026#34;: cerebro = bt.Cerebro() # 数字货币交易 taker 的手续费一般是这个数字。 cerebro.broker.setcommission(0.0005) cerebro.broker.set_slippage_perc(0.0001) # 初始资金为 1百万个货币单位 cerebro.broker.setcash(1e6) data = yf.download(tickers=\u0026#39;BTC-USD\u0026#39;, start=\u0026#39;2020-01-01\u0026#39;, end=\u0026#39;2025-04-16\u0026#39;, interval=\u0026#39;1d\u0026#39;, multi_level_index=False) cerebro.adddata(bt.feeds.PandasData(dataname=data)) cerebro.addstrategy(MACrossStrategy) print(\u0026#34;初始净值：\u0026#34;, cerebro.broker.getvalue()) cerebro.run() print(\u0026#34;最终净值：\u0026#34;, cerebro.broker.getvalue()) cerebro.plot() backtrader 中的 Cerebro 类型是核心大脑，其他的组件都是围绕着它进行配置。\n这段代码的核心内容包括：\n设置费率为万五，滑点为万1，初始资金为 1e6，即 1百万货币单位。 开始回测前，添加标的数据，通过 yfinance 下载数据，如加载 BTC-USD 的历史行情。 接着加载要回测的策略 MACrossStrategy，继承自 bt.Strategy。 最后运行回测并输出策略前后的账户净值数据并绘制图。 策略还没有实现任何的交易逻辑，如果运行回测，你会看到前后的账户资产没有变化。\n策略实现 # 实现一个策略有两个步骤，分别是计算指标和策略逻辑两个部分，分别对应于 MACrossStrategy 的 __init__ 和 next 函数。\n计算指标 # 计算这个策略用到的三个指标，分别是 short_ma、long_ma 和 atr。\nclass MACrossStrategy(bt.Strategy): params = ( (\u0026#34;short_period\u0026#34;, 10), (\u0026#34;long_period\u0026#34;, 20), (\u0026#34;atr_period\u0026#34;, 14), ) def __init__(self): self.short_ma = bt.ind.EMA(period=self.p.short_period) self.long_ma = bt.ind.EMA(period=self.p.long_period) self.atr = bt.ind.ATR(period=self.p.atr_period) # 短均线上穿长均线 self.crossover = bt.ind.CrossUp(self.short_ma, self.long_ma) self.crossunder = bt.ind.CrossDown(self.short_ma, self.long_ma) 这里将计算指标要用到的周期参数化，以便于后续优化。而且为了便于后续交易逻辑判断，通过 backtrader 的 crossover 和 crossunder 辅助函数判断均线的金叉死叉。\nbacktrader 支持的指标繁多，可通过几行代码拿到支持的常见指标：\nindicators = [attr for attr in dir(bt.ind) if not attr.startswith(\u0026#39;_\u0026#39;)] print(\u0026#34;Backtrader支持的内置指标:\u0026#34;) for idx, indicator in enumerate(sorted(indicators), 1): print(f\u0026#34;{idx}. {indicator}\u0026#34;) 输出：\nBacktrader支持的内置指标: 1. ADX 2. ADXR 3. AO 4. APO 5. ATR ... 如果熟悉 talib，也可以通过使用它。backtrader 内置支持了 talib。\nshort_ma = bt.talib.EMA(timeperiod=self.p.short_period) 开仓平仓 # 交易逻辑部分可分为开仓平仓和风险控制两部分。简单的开仓平仓实现起来较为简单，先介绍这部分的实现。而风险控制要依赖条件单实现，backtrader 是支持条件单的。\n开仓平仓逻辑的代码如下所示：\nclass MACrossStrategy(bt.Strategy): def __init__(self): pass def next(self): long_entry = ( self.crossup[0] == 1 and self.position.size \u0026lt;= 0 ) short_entry = ( self.crossdown[0] == 1 and self.position.size \u0026gt;= 0 ) long_exit = ( self.crossdown[0] == 1 and self.position.size \u0026gt; 0 ) short_exit = ( self.crossup[0] == 1 and self.position.size \u0026lt; 0 ) if long_entry: self.order_target_percent(target=0.99) elif short_entry: self.order_target_percent(target=-0.99) if long_exit: self.close() elif short_exit: self.close() 按设定规则开平仓，如果短线上穿长线开多，短线下穿长线开空，平仓的条件反之。\n下单方式没有使用 buy 和 sell 方法，直接用了 backtrader 提供的目标订单方法，省掉了下单数量的计算。如果想拿到直接下单大小，order_target_percent 返回了订单 Order，有订单大小 size 属性，这个后面设置止损条件是要用到的。\nbacktrader 有个非常重要的 Line 的概念，相对是比较复杂的，这里不展开说了。记住的是，如果要访问当前数据，下标都是 0 开始的，往前看就是 -1、-2，以此类推。所以，self.crossup[0] 就是检查当前短均线是否上穿长均线。\n暂时这里还没有加入风险控制，直接 0.99 的仓位，基本就是全仓了。\n现在可以运行下这个回测。\npython main.py 输出：\n初始净值： 1000000.0 最终净值： 3280739.0840676297 五年多的时间赚了 3 倍收益，和直接持有 BTC 的这五年收益相比，简直是浪费时间。\n完整代码请访问：双均线策略基础版本。\n绘图 # 回测结束除了可以拿到最终净值，运行 cerebro.plot() 还输出一张图，如下所示：\n这个图中有几个部分，分别是净值曲线、交易盈亏点、价格蜡烛图（包括买卖点标注）和技术指标。\n这个图的收益部分显示效果不是很清晰，如果想只看收益曲线，可通过配置 analyzers 拿到收益序列。\ncerebro.addanalyzer(bt.analyzers.TimeReturn, _name=\u0026#34;timereturn\u0026#34;) strat = cerebro.run() returns = strat[0].analyzers.getbyname(\u0026#39;timereturn\u0026#39;).get_analysis() returns_series = pd.Series(returns) net_value = (1 + returns_series).cumprod() ax = net_value.plot(title=\u0026#34;Returns\u0026#34;, figsize=(12, 5)) end_value = net_value.iloc[-1] end_index = net_value.index[-1] ax.text( end_index, end_value, f\u0026#34;End: {end_value:.2f}\u0026#34;, ha=\u0026#34;right\u0026#34;, va=\u0026#34;top\u0026#34;, bbox=dict(facecolor=\u0026#34;white\u0026#34;, alpha=0.8), ) plt.show() 净值曲线如下所示：\n其他数据，如行情、指标、交易记录等，然也可以拿出来单独绘图。\n完整代码请访问：自定义收益曲线。\n评价指标 # 从上面的净值曲线图，这个策略的回撤非常大。如果想看最大回撤的具体大小，要添加分析器。夏普比率是同样的思路。\ncerebro.addanalyzer(bt.analyzers.DrawDown, _name=\u0026#34;drawdown\u0026#34;) cerebro.addanalyzer(bt.analyzers.DrawDown, _name=\u0026#34;shape_ratio\u0026#34;) strat = cerebro.run() drawdown = strat[0].analyzers.getbyname(\u0026#34;drawdown\u0026#34;).get_analysis() sharpe_ratio = strat[0].analyzers.getbyname(\u0026#34;drawdown\u0026#34;).get_analysis() print(\u0026#34;最大回撤\u0026#34;, drawdown[\u0026#34;max\u0026#34;][\u0026#34;drawdown\u0026#34;]) print(\u0026#34;夏普比率\u0026#34;, shape_ratio[\u0026#34;shape_raito\u0026#34;]) 输出：\n最大回撤 82.12757289556565 夏普比率 0.5393603817365935 其他常见的评价指标，基本都可通过配置分析器拿到，如 SQN 指标，这个常用来选择参数优化的最优策略。\ncerebro.addanalyzer(bt.analyzers.SQN, _name=\u0026#34;sqn\u0026#34;) 策略优化 # 暂时先不加入风险控制，尝试优化下双均线策略。\nbacktrader 内置网格搜索优化的函数。将前面的 addstrategy 换成 optstrategy，指定参数搜索的范围。\ncerebro.optstrategy( MACrossStrategy, short_period=range(5, 50, 5), long_period=range(20, 200, 10), ) 为防止不满足条件的参数出现，如 short_period \u0026gt;= long_period，修改下策略实现：\nclass MACrossStrategy(bt.Strategy): def __init__(self): if self.p.short_period \u0026gt;= self.long_period: raise bt.StrategySkipError() 这样也能提升优化的效率。\nopt_returns = cerebro.run(optreturn=False) 运行后，将得到一个可遍历列表 opt_returns，它的元素是每个参数组合的数据。\nfor opt_return in opt_returns: if len(ret): print(\u0026#34;参数：\u0026#34;, ret[0].params.short_period, ret[0].params.long_period) print(\u0026#34;净值：\u0026#34;, ret[0].broker.getvalue()) print(\u0026#34;夏普：\u0026#34;, ret[0].analyzers.sharpe.get_analysis()) 现在通过 for 输出各个参数组合的回测结果。\n上面之所以要设置 optreturn=False 是为了拿到策略的完整数据，否则如 .broker.getvalue() 是无法访问的。\nBacktrader 的优化功能和 backtesting.py 不同，它的内置优化功能（cerebro.optstrategy() 和 cerebro.run()）不会自动选择最优策略。它只会执行所有参数组合的回测，并返回每个组合的结果。我还需要分析这些结果并选择最佳参数。\n如选择夏普比率最优的参数组合：\nopt_returns = cerebro.run(optreturn=False) best_strategy = None for ret in opt_returns: if len(ret): if best_strategy is None: best_strategy = ret[0] else: best_sharpe_ratio = best_strategy.analyzers.sharpe.get_analysis()[ \u0026#34;sharperatio\u0026#34; ] sharpe_ratio = ret[0].analyzers.sharpe.get_analysis()[\u0026#34;sharperatio\u0026#34;] if best_sharpe_ratio \u0026lt; sharpe_ratio: best_strategy = ret[0] short_period = best_strategy.params.short_period long_period = best_strategy.params.long_period sharpe_ratio = best_strategy.analyzers.sharpe.get_analysis()[\u0026#34;sharperatio\u0026#34;] max_drawdown = best_strategy.analyzers.drawdown.get_analysis()[\u0026#34;max\u0026#34;][\u0026#34;drawdown\u0026#34;] final_value = best_strategy.broker.getvalue() print(f\u0026#34;参数组合：short {short_period}, long {long_period}\u0026#34;) print(f\u0026#34;夏普比率：{sharpe_ratio}\u0026#34;) print(f\u0026#34;最大回撤：{max_drawdown}\u0026#34;) print(f\u0026#34;最终净值：{final_value}\u0026#34;) 输出：\n参数组合：short 10, long 90 夏普比率：0.9247734042607272 最大回撤：57.21188766636116 最终净值：12699593.957906708 你可以设定任意的标准来定义什么是最佳参数组合，backtesting.py 框架默认采用选择 SQN 作为选择评价最优策略标准，backtrader 也可以实现，分析加入 SQN 即可。\ncerebro.addanalyzer(bt.analyzers.SQN, _name=\u0026#34;sqn\u0026#34;) 优化就展开这么多吧！\n完整代码请访问：参数优化。\n风险控制 # 到现在策略就是简单通过均线金叉死叉进行开平仓，完全按照这个标准交易，有可能出现损失过大。\n如上图所示，短线上穿长线开仓做多后，开始快速下跌，从开仓到死叉平仓的损失不可能控，有可能导致异常的损失。\n我想通过设置止损价格解决这个问题，为防止被假止损，采用 n * ATR 计算止损位置。ATR 指标是个波动指标，如果波动太大可能导致不可控的损失。可基于最大损失百分比去反向推算下单金额，如每次最大损失不超过 2%。\n假设 n = 3，在策略中实现这个逻辑，示例代码如下：\ntarget_percent = 0.02 / (3 * self.atr[0] / self.data.close[0]) 用这个值替代之前每次满仓的 0.99，实现每单的最大损失为总资产的 2%。\n多仓的止损价格：\nstop_price = self.data.close[0] - 3 * self.atr[0] 止损单通过 backtrader 的 stop 类型订单实现。\n基于开仓订单创建止损订单：\norder = self.order_target_percent(target=target_percent) self.stop_sell_order = self.sell( size=order.size, exectype=bt.Order.Stop, price=stop_price ) 开空单同理：\norder = self.order_target_percent(target=target_percent) self.stop_buy_order = self.sell( size=order.size, exectype=bt.Order.Stop, price=stop_price ) 这里有个问题，如果仓位平掉后，记得要取消这个止损单，可通过 backtrader 的 notify_trade 回测检查仓位，如发现仓位非做空仓位，取消 stop_buy_order。\n现在，默认参数（short_period=10，long_period=20）运行回测，净值曲线如下：\n可以对比下最初的净值曲线：\n这里只是增加了一个基本的风险控制，现在的净值曲线比之前稳定了不少。\n完整代码请访问：风险控制。\n但这还有明显的大涨大跌，如果已经设置了最大损失百分比，应该不至于出现还有这么可怕的波动。\n初步估计这个原因可能是行情数据用的日线数据，价格跳动太大导致的滑点严重。而一旦回撤太大，后续想要翻身的难度也会增加。\n接下来，尝试用 backtrader 加载分钟数据降低滑点。\n重放分钟行情 # backtrader 提供了数据重放 replaydata 加载数据，实现近乎模拟真实的场景。重放 replaydata 通过重放高频数据，事实生成某个周期的行情，如日线、4 小时等，即使这个周期还没闭合。\n我提前下来了 BTCUSDT 的 1分钟数据，将其通过 replaydata 加载到 cerebro 中。\ndata = pd.read_csv( \u0026#34;btcusdt_1m\u0026#34;, parse_dates=[\u0026#39;datetime\u0026#39;], index_col=[\u0026#39;datetime\u0026#39;], ) cerebro.replaydata( data, timeframe=bt.TimeFrame.Minutes, compression=1440, ) 这个地方有一个注意点，如果原始数据是分钟级别的数据，replaydata 的 timeframe 只能是 Minutes，通过 compression 指定合成日线所需的分钟数据量（一天等于 60 * 24 = 1440）。如果是其他周期，如 bt.TimeFrame.Days，重放是无法合成日线数据的。\n策略逻辑的判断部分也有要修改的地方：\nlong_entry = self.crossup[-1] and self.position.size \u0026lt;= 0 short_entry = self.crossdown[-1] and self.position.size \u0026gt;= 0 long_exit = self.crossdown[-1] and self.position.size \u0026gt; 0 short_exit = self.crossup[-1] and self.position.size \u0026lt; 0 将原来的获取最新的指标修改为上个周期的指标，原因当然就是，当前的 bar 还没有闭合。\n这个回测耗时相当长，最终得到的回测净值曲线：\n从上图看出，通过分钟回测，得到的净值曲线更平缓，虽然从 2022 年以后这个策略不怎么赚钱了，但不至于那么过分的大涨大跌，产生过大的风险。\n这里还有个小问题，如果看上图，偶尔有几个月空仓的情况，那是因为如果是止损单平仓，而非出现金叉死叉触发平仓，可能导致无法重新入场导致丢失掉大趋势。\n重新修正下开仓的判断条件。\nif self.crossup[-1] == 1: self.direction = \u0026#34;long\u0026#34; elif self.crossdown[-1] == 1: self.direction = \u0026#34;short\u0026#34; long_entry = ( self.direction == \u0026#34;long\u0026#34; and self.data.close[-1] \u0026gt; self.short_ma[-1] \u0026gt; self.long_ma[-1] and self.position.size \u0026lt;= 0 ) short_entry = ( self.direction == \u0026#34;short\u0026#34; and self.data.close[-1] \u0026lt; self.short_ma[-1] \u0026lt; self.long_ma[-1] and self.position.size \u0026gt;= 0 ) 在出现金叉死叉时才切换下单方向。这个就不想过多解释了。\n净值曲线如下：\n完整代码请访问：重放模拟回测。\n总结 # 本文介绍了如何用 backtrader 回测双均线策略，一步步展开。虽然是一个简单的均线策略，但如果无法正确利用回测框架，可能产生的结果差异较大。\n这个策略表现的并不算好，5 年时间在 BTC 上只有不到 4 倍的收益，有兴趣的话，可继续优化，如将日线切换为小时线，或许会有新的发现哦。\n最后特别说明：本文是我用 backtrader 回测双均线的记录，具体代码在文中都有提供，或许有错误，请仔细甄别。\n最后，希望本文对你学习 backtrader 策略回测有所帮助。\n","date":"2025-04-15","externalUrl":null,"permalink":"/posts/2025-04-16-macross-using-backtrader/","section":"文章","summary":"我花了一个星期通读了 backtrader 的官方文档，順便还通过 AI 整理出了它的中文文档，我不想写 backtrader 的基础入门教程，还不如直接用 backtrader 逐步展开回测这一个策略，或许更加容易掌握它的使用。\n","title":"掌握 backtrader 策略回测：均线交叉交易系统","type":"posts"},{"content":"翻译：Pairs Trading for Beginners: Correlation, Cointegration, Examples, and Strategy Steps\n配对交易策略是在两只具有协整关系的股票之间寻找交易机会时最流行的策略之一。\n如何形成协整关系？如何利用配对交易策略从它们的协整关系中获利？\n本篇博客将全面探讨这些问题，内容涵盖：\n什么是配对交易？ # 在配对交易策略中，通常会以市场中性方式交易两只股票，即无论市场处于上涨还是下跌趋势，两只股票的开仓头寸都能相互对冲。\n配对交易策略的关键挑战在于：\n筛选具备长期统计套利潜力的股票组合 精准把握入场与出场时机 配对交易的历史 # 配对交易策略最早出现于20世纪80年代中期，由摩根士丹利雇佣的一批技术分析师研究团队首创。该策略通过统计分析与技术分析相结合的方法，寻找潜在的市场中性盈利机会。\n配对交易的底层逻辑是什么？ # 配对交易策略的核心在于：\n两只股票或金融工具的价格走势应当围绕相似均值波动，并保持相对稳定的价差关系。然而在某些情况下，其中一只标的价格可能出现短期偏离。\n当这种价差偏离发生时，交易者可以抓住机会做多被低估的金融工具，同时做空被高估的一方。具体头寸建立需综合考量两只股票的实时市场价格及其历史标准差。\n配对交易核心术语解析 # 在配对交易策略中，有一些关键术语需要掌握。\n相关性 # 相关性通过相关系数ρ量化表示，其取值范围为-1至+1。该系数反映两个变量之间的关联程度：\n相关性分析 # 相关性通过相关系数ρ量化表示，其取值范围为-1至+1。该系数反映两个变量之间的关联程度：\n完全正相关（+1）：当一个变量上涨或下跌时，另一个变量同向等幅变动 完全负相关（-1）：当一个变量上涨时，另一个变量等幅反向变动 零相关（0）：两个变量不存在线性关联关系 完全正相关指的是：当一个变量无论上涨或下跌时，另一个变量始终保持完全同向且等幅度的变动。\n而完全负相关则表现为：当一个变量上涨时，另一个变量呈现完全反向但幅度相等的下跌走势（反之亦然）。\n两个变量的相关系数计算公式如下：\n$$相关系数(X,Y) = ρ = COV(X,Y) / [SD(X) \\cdot SD(Y)]$$其中：\nCOV(X, Y) = X与Y的协方差 SD(X) 和 SD(Y) = 各自变量的标准差 当相关系数较高时（例如达到0.8），交易者可能会选择该股票对进行配对交易。这一高数值表明两只股票之间存在强关联性——若股票A上涨，股票B同步上涨的概率也相当高。\n基于这一假设，交易者会采取市场中性策略——买入股票A同时卖出股票B，买卖决策依据两只股票各自的走势模式而定。\n但仅凭相关性分析可能导致伪相关结论。例如，若配对交易策略基于两只股票的价格差（价差=log(a) - nlog(b)，其中a、b分别代表股票A、B的价格），可能出现两只股票价格持续同向增长而永不均值回归的情况。\n价差 = log(a) - nlog(b)\n当a、b价格同步上涨，但价差持续收窄时（即股票A涨幅小于股票B），由于做空了涨幅更大的股票B，最终将导致投资亏损。\n因此，在执行配对交易策略时，仅依靠相关性指标筛选股票对存在风险，需谨慎对待。\n协整性 # 配对交易最常用的检验方法是协整检验。协整是指两个或多个时间序列变量之间存在这样的统计特性：它们的线性组合能够形成平稳序列。\n具体来说，这里的两个时间序列变量分别是股票A和股票B的价格对数。这两个变量的线性组合可以构成定义价差的线性方程：\n价差 = log(a) – nlog(b)\n其中 a 代表股票A的市场价格，b 代表股票B的市场价格。操作上，每买入1单位股票A，需卖出n单位股票B\n若股票A和B存在协整关系，则上述方程具有平稳性。平稳过程所具有的宝贵特性，正是构建配对交易策略所需的关键条件。\n例如在本例中，若该方程平稳，则意味着方程的均值保持长期稳定，方差不会随时间发生显著变化。\n假设初始设定对冲比率n使价差=0，根据平稳性特征，价差的期望值将持续保持为0，任何偏离该期望值的情况都构成统计异常，这种异常正是实施配对交易的绝佳时机！\nZ分数（标准分数） # 当原始数据服从正态分布时，通过Z分数转换可将其转化为标准正态分布（均值为0，标准差为1）。在配对交易策略中的应用时，这种标准化处理对于设定交易阈值至关重要。\n例如，在配对交易中，有股票A和B价格之间价差的分布。我们可以将这些原始价差分数转换为Z分数。\n这个新分布将具有均值为0和标准差为1的特性。很容易为此分布创建阈值水平，如1.5sigma、2sigma、2.5sigma 等。\nZ分数的计算公式如下：\n$$ z = (x – 均值) / 标准差 $$其中,\nx = 原始数据点 z = Z分数\n均值和标准差可以是基于 \u0026rsquo;t\u0026rsquo; 天、分钟或时间间隔内的滚动统计数据。\nADF 检验 # ADF 检验是标准 DF 检验的扩展，用于检验时间序列的平稳性和非平稳性。\n与 DF 检验的主要区别在于，ADF 检验可应用于大规模时间序列模型。由于大规模时间序列模型可能更为复杂，因此将 DF 检验改进为 ADF 检验。此外，ADF检验能够处理含有缺失值的数据。\n配对交易步骤 # 配对交易选股标准 # 在实施配对交易策略时，所选股票组合必须满足时间序列平稳性要求。平稳的时间序列能确保预测结果准确可靠。\n此外，时间序列平稳性表明股票对具有协整关系，可通过交易信号进行配对交易。因此，选股是开展配对交易的关键环节。\n理解平稳时间序列是正确选择配对交易股票的核心。当时间序列呈现平稳性时，不仅能保证预测准确性，还能确认股票间的协整关系，从而生成可靠的交易信号。深入掌握这一概念将显著提升您的配对交易策略成效。\n对于任意两只股票，价差定义如下：\n价差 = log(a) - nlog(b)\n其中： a = 股票A的价格 b = 股票B的价格\n假设：对冲比率n为常数\n通过回归分析计算n值，使价差尽可能接近0。因此，我们通过对股票价格进行回归来计算对冲比率。\n理论依据：在回归分析中，我们会得到一个称为\u0026quot;残差\u0026quot;的项，它表示观测值与拟合曲线或估计值之间的距离。这些残差告诉我们，对于计算得出的n值，实际\u0026quot;价差\u0026quot;与0的偏离程度。\n我们通过研究这些残差来判断它们是否呈现趋势性。如果残差没有形成趋势，则意味着价差随机围绕0波动，呈现平稳特性。\n对价差序列（代入计算得到的n值）进行 ADF 检验。\nADF 检验是一种假设检验，其检验结果以 p 值呈现。若p值小于0.05或0.01，我们就能以95%或99%的置信度判定该价差序列具有平稳性，从而选择该股票配对。\n至此，我们已经探讨了统计套利在选股过程中面临的挑战及相关的统计方法。通过协整检验，我们可以在特定置信区间内确认两只股票的价差序列具有平稳性——即该信号存在均值回归特性。价差计算公式为：\n价差 = log(a) - nlog(b)\n其中a、b分别代表股票A和B的价格，每买入1单位股票A需卖出n单位股票B，n值通过两只股票的价格回归分析得出。\n在确认该价差序列具有均值回归特性后，我们需要确定其波动的极端点位或阈值水平。当价差突破这些阈值时，将触发配对交易指令。\n为有效识别这些阈值水平，配对交易中广泛采用名为\u0026quot;Z分数\u0026quot;的统计构建方法。\n入场点位设定 # 首先定义价差为\u0026rsquo;s\u0026rsquo;： 价差 = s = log(a) - nlog(b)\n基于\u0026rsquo;t\u0026rsquo;时间周期的滚动均值和标准差，计算\u0026rsquo;s\u0026rsquo;的z分数，记为\u0026rsquo;z\u0026rsquo;。\n将阈值设定在1.5σ至2σ之间（该参数需通过回测优化确定，避免过拟合风险）。\n当z分数突破上阈值时，执行做空操作：\n卖出股票A 买入股票B 当z分数突破下阈值时，执行做多操作：\n买入股票A 卖出股票B 需严格保持对冲比例计算交易数量\n至此我们已阐明配对交易的入场机制，接下来将探讨交易的另一关键环节——出场点位判定。\n出场点位设定 # 止损\n止损旨在应对预期走势未出现的情况。\n例如，当我们选择 2σ 作为入场信号时，预期价差将从该阈值回归均值。但价差可能持续扩大，假设达到2.5σ时已产生亏损。\n为控制风险，可将止损位设在3σ水平。\n除设置固定止损阈值（如 3σ 或偏离均值的极端值）外，还需持续监测协整关系。若持仓期间协整关系被破坏，则应立即平仓，因该交易的基本假设已不成立。\n止盈\n止盈用于在价格反向波动前锁定利润。例如，当您做多价差时（即根据本文定义买入股票A并卖出股票B），预期价差将回归均值或零值。在盈利情况下，当均值从阈值回归后首次穿过零线时，即可触发止盈。\n止盈策略可有多重设定方式，具体取决于你的风险偏好和历史回测结果。\n实际交易中，一些因素至关重要，如你的交易经验、综合技能体系、全面分析能力。正如前面强调的，风险承受能力与回测结果将共同决定最优止盈策略。自动化执行与实战应用是本策略的关键。\n回顾下，配对交易是通过建立统计关联资产的多空对冲组合来实现的交易策略。该策略本质上是均值回归策略，其核心逻辑是押注价格将回归历史趋势。\nPairs trading strategy # 构建配对交易策略的首要步骤是确保股票对具有协整关系。只有当股票对通过协整检验后，才能被纳入配对交易策略。为验证协整关系，我们采用 ADF 检验。\n开展配对交易必须制定完整的交易策略。在实盘交易前，必须全面评估策略的各项参数，包括：\n最大回撤； 平均盈利交易； 平均亏损交易； 总体盈亏交易； 等等。\n配对交易的优势 # 配对交易具有以下优势：\n降低潜在亏损和风险 # 当配对交易策略按预期运作时，能有效控制潜在亏损。该策略通过同时操作两只证券来分散风险，若其中一只表现不佳，另一只可抵消部分损失。\n稳定收益 # 配对交易策略能使交易者在任何市场条件下获得稳定收益。交易者通过捕捉股票价格偏离均值的机会来实现盈利。\n对冲保护 # 配对交易最大的优势在于完全对冲。通过做空高估证券和做多低估证券，天然形成对冲机制，从而限制亏损风险。\n配对交易的劣势 # 配对交易存在以下缺点：\n高度依赖统计相关性 # 配对交易要求证券间具备高度统计相关性。多数交易者要求至少0.80的相关系数，这在实际操作中较难实现。\n高昂佣金成本 # 由于佣金费用较高，部分交易者不建议采用配对交易。有时单次配对交易产生的佣金近乎标准交易的两倍。\n价格执行风险 # 配对交易的盈利依赖微薄价差，通常需要大额交易量。这导致在开仓时难以按预期价格成交的风险升高。由于交易量庞大，证券买卖价格的微小差异都可能对最终收益产生重大影响。\n总结 # 配对交易是一种基于高度相关性证券在出现偏离后将回归中性位置的假设的交易策略。该策略可应用于各类市场和交易品种，如股票、外汇等。必须谨慎评估证券相关性，因为任何错误的假设或预测都可能导致配对交易策略失败。\n如果您是初学者并希望深入了解配对交易策略，我们建议您从这套专门适合配对交易初学者的均值回归策略学习课程开始。该课程包含多个教学模块，能帮助您系统掌握配对交易技能。\n","date":"2025-04-04","externalUrl":null,"permalink":"/posts/2025-04-04-pairtrading-for-begginers/","section":"文章","summary":"翻译：Pairs Trading for Beginners: Correlation, Cointegration, Examples, and Strategy Steps\n配对交易策略是在两只具有协整关系的股票之间寻找交易机会时最流行的策略之一。\n如何形成协整关系？如何利用配对交易策略从它们的协整关系中获利？\n","title":"配对交易基础教程","type":"posts"},{"content":"本文继续介绍一个常见的套利策略，三角套利策略。我将介绍如何实时监控加密货币现货市场上的三角套利机会。\n如果你看过我的前几篇文章，应该知道我最近一直专注在数字货币套利策略的学习研究。至于是否有低费率权限、硬件配置、低延迟网络等客观条件，不是文章的重点。我更多还是放在介绍套利的主线逻辑，完成我的整个套利系列的文章和工具集。请把它们当成一个抛砖引玉的 demo 即可，当你有这方面需求，能给你提供一些参考和思考，那就是好的。\n正式开始本篇文章。\n什么是三角套利？ # 三角套利原本是一种针对外汇市场的套利策略，通过利用三种货币对间的汇率失衡实现无风险利润。其核心原理基于“交叉汇率”一致性。\n例如有 A/B、B/C、C/A 三种货币对，它们的实际价格与理论换算值有偏差，即按 A→B→C→A 兑换路径得到的 A 价值高于兑换前 A 的价值，我们即可进行三步循环交易，锁定汇率修正前的差价收益。\n这个策略同样适用于数字货币市场。\n如某交易所内的三个交易对 BTC/USDT、ETH/BTC 和 ETH/USDT，它们的价格分别是 30000、0.05 和 1520。假设无滑点且每次手续费为 0.1%。\n现按路径 USDT→BTC→ETH→USDT 完成兑换。\nUSDT -\u0026gt; BTC: 10000 USDT 买入 10000 / 30000 = 0.3333 BTC； BTC -\u0026gt; ETH: 0.3333 BTC 兑换 0.3333 / 0.05 = 6.666 ETH； ETH -\u0026gt; USDT: 6.666 ETH 卖出 6.666 * 1520 = 10132 USDT； 三次交易即要扣除手续费（约 10000 * 0.001 * 3 = 30 USDT）后净收益约 132-30 = 103 USDT。\n这是不是看似很美好。\n实际上，随着加密货币市场逐渐成熟，这种纯理论套利机会也不是很多。且这种操作通常依赖自动化瞬时完成，需精确计算成本与收益。\n实时监控工具 # 我实现了这样一个简单监控工具，看看当前市场是否存在这样的套利机会。\n工具仓库地址 github.com/poloxue/seekoptrader\n效果如下：\n$ export PYTHONPATH=`pwd` $ python seekoptrader/__main__.py triangle --exchange-name okx 如上图有六个汇率值，这是因为以不同币种作为起点和选择不同的兑换路径，一个三角配对可以有六种汇率计算方式。\n假设你的费率是 0.1%，不考虑滑点的情况下，如果你能发现汇率大于 1.003 的机会，就有机会获利。如果是质押借贷，还要考虑借贷利息。\n注：暂时不建议在 binance 上尝试，因为它的上面币种太多了，跑不动。不过或许它有机会的概率会高些。\n实现思路 # 这个工具的实现和跨市场价差的监控类似，大致分三个步骤：\n首先，匹配到可形成三角闭环交易对，毕竟希望监控能尽量覆盖全量市场； 其次，实时监控订单薄消息，这个和之前的跨市场类似，为了提高实时性，建议批量监控； 最后，计算三角汇率，我考虑了正反交易方向与不同计价币种的组合，将会计算出六组汇率； 与跨市场价差不同之处是，交易对的三角匹配和计算公式。\n三角交易对匹配 # 三角套利的匹配比起跨交易所的匹配要复杂些，说下我遇到的有几个点吧。\n匹配限制 # 首先，前面说三角套利是基于 A/B、B/C、C/A 这样的交易对组合实现闭环交易。加密货币市场上，基本都是按照 B/A（BTC/USDT）、C/B（ETH/BTC）、C/A（ETH/USDT）形成三角配对。\n我不了解外汇市场的币对是什么规则，可能更加灵活吧。\n为了便于确定汇率计算公式，我严格限制了交易对必按 B/A -\u0026gt; C/B -\u0026gt; C/A 格式和顺序的才能匹配成功。这样一来， 在确定基准单位（兑换起点）和兑换路径后，汇率公式就能确定下来了。不过这样不知道会不会溜掉一些交易对。\n如 A -\u0026gt; B -\u0026gt; C -\u0026gt; A 的兑换路径：\n1 / ask(B/A) / ask(C/B) * bid(C/A) 而 B -\u0026gt; C -\u0026gt; A -\u0026gt; B 的兑换路径：\n1 / ask(C/B) * bid(C/A) / ask(B/A) 其中的 ask 和 bid 表示最优卖价和买价。\n过滤规则 # 在用 ccxt 加载现货市场交易对时，发现了法币加密币的交易对，这些大概率无法交易，我直接过滤掉了。我梳理了几个主流交易所（binance、bybit、okx、bitget 和 coinbase）的法币，或许还有遗漏吧。\n此外，我还设置了一个条件，过滤掉存在两个稳定币的三角，防止数量过于膨胀。基本思路是，稳定币的价格变化太小，资源有限的情况下，优先监控非稳定币。\n代码片段 # 如下是加载配对某交易所可形成三角闭环交易对的代码：\ndef find_triangles(self, markets): G = nx.DiGraph() for m in markets: base, quote, symbol = m[\u0026#34;base\u0026#34;], m[\u0026#34;quote\u0026#34;], m[\u0026#34;symbol\u0026#34;] G.add_edge(base, quote, symbol=symbol) triangles = {} currencies = G.nodes() for b in currencies: for a in G[b]: for c in currencies: if c == b or c == a: continue if c in G and b in G[c]: if a in G[c]: if self.valid_currencies([b, a, c]): triangles[(a, b, c)] = ( G[b][a][\u0026#34;symbol\u0026#34;], G[c][b][\u0026#34;symbol\u0026#34;], G[c][a][\u0026#34;symbol\u0026#34;], ) return triangles 这里用了一个网络拓扑工具库 networkx 构建了一个有向图，从中搜索可形成闭环的三个交易对。其中的 valid_currencies 函数用于过滤一些配对，如法币或稳定币交易对。\n兑换汇率计算 # 因为要更多可能监控交易机会，不限制兑换路径和计价币种。如何理解呢？\n还是以 BTC/USDT、ETH/BTC 和 ETH/USDT 为例。\n最容易想到的兑换路径是 USDT -\u0026gt; BTC -\u0026gt; ETH -\u0026gt; USDT，但也可以是 USDT -\u0026gt; ETH -\u0026gt; BTC -\u0026gt; USDT，这两个路径都是以 USDT 为计价币。我还可以从 ETH 为基点，即 ETH 计价，那么兑换路径就是 ETH -\u0026gt; USDT -\u0026gt; BTC -\u0026gt; ETH，或是 ETH -\u0026gt; BTC -\u0026gt; USDT -\u0026gt; ETH。\n一旦路径和基点变了，那计算的公式也就完全不同了。\n如 USDT -\u0026gt; BTC -\u0026gt; ETH -\u0026gt; USDT 的兑换路径，公式是：\n1 / ask(BTC/USDT) / ask(ETH/BTC) * bid(ETH/USDT) 而 BTC -\u0026gt; ETH -\u0026gt; USDT -\u0026gt; BTC 的兑换路径，公式是：\n1 / ask(ETH/BTC) * bid(ETH/USDT) / ask(BTC/USDT) 这样组合起来，一个三角配对就有了 6 个交易路径，这也是为什么上面演示的工具中有 6 个 汇率的原因。\n现在很多币种都可以质押借款，如果能抓住这瞬时的交易机会，在还掉借贷后，还能赚点币在手里，这就是利润。\n灵活推算汇率公式 # 除了前面按交易对模式固定死汇率公式，还可以更灵活的推算公式，只要提供兑换起点币种和三角交易对，就能推出三角兑换的汇率公式。\n我已经把这个思路的核心逻辑代码写出了，借这篇文章保存下，万一哪天需要还能找到。\nfrom collections import defaultdict def detect_arbitrage_ops(start_currency, symbols): graph = defaultdict(dict) for symbol in symbols: base, quote = symbol.split(\u0026#34;/\u0026#34;) graph[base][quote] = symbol # 正向交易 base→quote graph[quote][base] = symbol # 反向交易 quote→base chains = [] max_depth = 3 def dfs(current, path, ops=[], depth=0): if depth == max_depth: if current == start_currency: chains.append(ops) return for next_currency in graph[current]: symbol = graph[current][next_currency] base, quote = symbol.split(\u0026#34;/\u0026#34;) new_op = f\u0026#34;*bid({symbol})\u0026#34; if current == base else f\u0026#34;/ask({symbol})\u0026#34; dfs(next_currency, path + [next_currency], ops + [new_op], depth + 1) dfs(start_currency, [start_currency]) return [\u0026#34;1\u0026#34; + \u0026#34;\u0026#34;.join(chain) for chain in chains] 示例测试:\nbase = \u0026#34;ETH\u0026#34; pairs = [\u0026#34;BTC/USDT\u0026#34;, \u0026#34;ETH/BTC\u0026#34;, \u0026#34;ETH/USDT\u0026#34;] operations = detect_arbitrage_ops(base, pairs) print(\u0026#34;ETH 兑换起点:\u0026#34;, operations) 输出：\nETH 兑换起点: [ \u0026#39;1*bid(ETH/BTC)*bid(BTC/USDT)/ask(ETH/USDT)\u0026#39;, \u0026#39;1*bid(ETH/USDT)/ask(BTC/USDT)/ask(ETH/BTC)\u0026#39; ] 现在的已经简单够用，就没把这个设计集成到工具里。\n总结 # 本文简单介绍了三角套利，开发了一个监控小工具。这是我的一次尝试，如有建议请提出来。如果你对代码实现感兴趣，可查看：triangle/monitor.py。\n最后，希望本文对你有用。\n最近感冒不舒服，脑子有点不清楚，希望文中不要有太多错误。\n","date":"2025-03-19","externalUrl":null,"permalink":"/posts/2025-03-19-triangle-arbitrage-in-crypto-market/","section":"文章","summary":"本文继续介绍一个常见的套利策略，三角套利策略。我将介绍如何实时监控加密货币现货市场上的三角套利机会。\n如果你看过我的前几篇文章，应该知道我最近一直专注在数字货币套利策略的学习研究。至于是否有低费率权限、硬件配置、低延迟网络等客观条件，不是文章的重点。我更多还是放在介绍套利的主线逻辑，完成我的整个套利系列的文章和工具集。请把它们当成一个抛砖引玉的 demo 即可，当你有这方面需求，能给你提供一些参考和思考，那就是好的。\n","title":"实时监控加密货币现货市场的三角套利机会","type":"posts"},{"content":"数字货币市场因为波动大，存在一些低风险套利机会，如前面介绍过的 资金费率套利策略，利用的是现货与永续合约对冲赚取资金费率长期为正的收益。\n本文继续介绍加密货币市场上的另一个低风险套利策略：期现套利。\n什么是期现套利？ # 期现套利与资金费率套利有类似点，也是通过对冲实现低风险套利，它的两个对冲标的是由相同底层资产的现货和交割合约完成。\n举个简单例子吧。\n在 BTC 现货价格为 10,000 美元、1个月期货价格为 10,200 美元时，我们买入现货并同步卖出等量期货合约，如 1 BTC，即可锁定200美元价差。到期后以期货价交割现货，实现 2% 的月回报。\n若计入交易成本（如双向 0.3% 手续费+滑点）净收益从200美元降至170美元。如果还有资金成本，如杠杆交易有借款利率，这部分也有减掉。\n说白了，这种套利就是等期货价格比现货高出一截时，一手买现货，一手卖期货，赚中间的差价。但也不是稳赚不赔——价差得能覆盖手续费、借款利息这些成本，而且得拿这两个单子等合约到期（如一个月），差价才会真正落袋为安。\n这个策略和跨交易所那种看到价差立刻倒手赚快钱不一样，这玩法更考验耐心和资金量，所以一般都是机构大资金在玩，大部分人没资金且没耐心，不愿意等这个时间赚小钱。\n监控价差 # 先一起感受下这个价差回归的过程，我以 OKX 上的 BTC-USDT-20250328 与现货 BTC/USDT 为例，下面是这个它们的价格历史变化图。\n可以看到，现货和合约的价格随着时间不断靠近。\n如果是价差走势图的话，会更加直观。你会看到价差会随着时间不断在向零靠近。\n绝对值：\n百分比：\n可以看到，12 月份牛市高点的时候，3 个月到期的期现价差大概 5.3%，这个收益实在不要任何操盘的情况下，基本确定能拿到的收益。\n如果是 BTC-USDT-20250627 的价格交割合约，大概有 9% 的价差。\n如何监控这个价差呢？\n一些交易图表软件或者交易所可能会提供监控这个价差的工具。如果有编程能力，可以自己开发。\n虽说期现套利并非捕捉瞬时价差机会，但我开发的那个简单的跨市场价差监控工具也能监控期现价差。不知道的可查看之前的文章 开发了一个加密货币跨市场价差监控工具\n$ python main.py --market-a okx.spot --market-b okx.future.linear 因为最近行情不好，期现套利的价差也很小了。\n如何操作呢？ # 简单点就手动操作，买入相同基准单位的仓位，如同时开仓 1 BTC 的现货和 1 BTC 的合约空头。\n也可以基于交易所的策略工具，如 OKX 有个套利策略。\n你可以按设置成交方式，如一腿按限价单下单，成交的同时，立刻用市价开仓另一腿。\n资金利用率 # 为了最大化资金利用率，可考虑将账户模式切换成跨币种保证金，这样能最大化提高资金率，还能减少爆仓风险。\n假设，我希望实现 1 万 USDT 的 BTC 期现套利。\n若采用传统单币种模式，无杠杆情况下，为了完全对冲，合约要 50% 保证金，现货只有 5000 USDT 可买入 BTC，若想实现 1 万的套利效果，只有继续追加 1 万 USDT。\n不过这种模式也有办法提高资金利用率。\n因为合约是支持杠杆的，为了实现合约对现货的完全对冲，不一定要同步保持 1 万保证金，可考虑 5 倍杠杆，这样也只要追加 20% 资金，即 2000 USDT 即可。\n这个模式存在两个问题，一个是资金利用率，还有就是爆仓概率大。以 20% 保证金率为例，空头的合约仓位就会爆仓。我们要经常看盘调整仓位。\n说说跨币种保证金模式，它会将其他币种按某个折算率统一折算成美元单位的信用额度，这个额度可作为合约保证金。\n还是之前的案例，这时的 1 万 USDT 可全额买入 BTC 现货。这部分 BTC 按 95% 抵押率折算成 9500 USDT 信用额度，可完全覆盖合约 USDT 保证金需求。此时现货无需减少持仓，合约也无需额外冻结资金，1 万 USDT 即可完成完整套利。\n这个模式还有个好处，降低爆仓概率。\n因为 BTC 现货与合约空头形成完全对冲，若价格剧烈波动，现货升值会同步增加抵押物价值（例如BTC上涨10%时，现货价值升至11000 USDT，抵押额度增至 10450 USDT），相当于动态强化了保证金安全垫，进一步降低爆仓概率。\n总结 # 本文介绍了加密货币市场上基本的期现套利策略，和资金费率套利类似，是一种与市场涨跌无关的低风险套利策略。\n最后，希望本文对你有用！\n","date":"2025-03-09","externalUrl":null,"permalink":"/posts/2025-03-10-risk-free-cash-and-carry-strategy-in-crypto-market/","section":"文章","summary":"数字货币市场因为波动大，存在一些低风险套利机会，如前面介绍过的 资金费率套利策略，利用的是现货与永续合约对冲赚取资金费率长期为正的收益。\n本文继续介绍加密货币市场上的另一个低风险套利策略：期现套利。\n什么是期现套利？ # 期现套利与资金费率套利有类似点，也是通过对冲实现低风险套利，它的两个对冲标的是由相同底层资产的现货和交割合约完成。\n","title":"加密货币市场的低风险套利策略-期现套利","type":"posts"},{"content":"本周写点国内期货的内容，介绍如何通过 Python 计算期货主连合约的复权数据。之所以要处理这个数据，是因为它会影响策略（如 CTA 策略）的回测结，是个大坑。新手要理解下期货与股票的差异。\n为便于后续使用，我将本文内容整合成了一个期货复权数据下载命令，如下是它的帮助信息：\nUsage: main.py [OPTIONS] Options: --ts-code TEXT 期货合约代码（例如：LH.DCE 表示生猪期货） [必填] --start-date TEXT 起始日期（格式：YYYYMMDD，默认为20220101） --adjust 复权方式（默认：forward）: forward: 前复权（以当前价格为基准调整历史数据） backward: 后复权（保持历史价格不变调整未来数据） --method 换月调整方法（默认：pre_close/pre_close）: pre_close/pre_close: 使用前收盘价复权 open/pre_close: 使用开盘价/前收盘价复权 pre_settle/pre_settle: 使用前结算价复权 open/open: 使用开盘价复权 --no-download 是否禁用复权数据下载（默认下载） --plot 是否显示价格曲线图（默认不显示） --help Show this message and exit. 支持了常见的主连合约的复权方法，先给大家效果，直观看看考虑复权和不复权价格数据的差异。\n以生猪为例：\npython main.py --ts-code LH.DCE --method pre_close/pre_close --plot 后复权价格图如下：\n如果你想做期货的量化策略，特别是 CTA 策略，用主连价格回测，与实盘交易差异将会很大。\n希望这能助新手避坑。\n这个脚本还会直接将复权数据下载到本地，便于后续回测使用。\n$ ls LH.DCE_backward_pre_close-pre_close.csv 数据样例：\nts_code adj_open adj_high adj_low adj_close date 2021-01-08 LH.DCE 29500.0 30680.0 26385.0 26810.0 2021-01-11 LH.DCE 26225.0 26720.0 26030.0 26030.0 2021-01-12 LH.DCE 25760.0 26210.0 25205.0 25560.0 2021-01-13 LH.DCE 25830.0 26350.0 25715.0 25870.0 2021-01-14 LH.DCE 25805.0 26155.0 24860.0 24890.0 前复权也支持。\npython main.py --ts-code LH.DCE --adjust forward --method open/pre_close --plot 价格图如下：\n如果没有 Tushare 数据，文中也提供了关于如何自定义切换规则的一些思路和代码。或者联系我拿 Tushare 权限，有 20% 的折扣。如果是临时需要，我可以免费提供一个品种的复权数据。\n如果不想看完整文章，直接看文件代码地址：download_future_adj_data.py。这个代码是完整版，文章重点是介绍实现逻辑。\n背景概述 # 最近希望合理分配资金、对冲风险，就计划测试些适用于国内市场的策略，如套利、CTA 策略，但 A 股不支持做空机制，于是转而选择了期货市场。\n但问题是，期货不同于股票，不是一个品种一个可交易标的，而是由连续的交割合约组成，即每个合约都有到期日，而同一品种的不同月合约都是有价差的。\n如下是玻璃2505和2509的合约的价格曲线：\n查看交易软件上的主合约价格图表，如下图所示是生猪的主力合约的价格图：\n你会发现，这其中存在一些明显的跳空。这些跳空基本就是主连合约切换合约时产生的。\n如果是短线交易或者跨期套利，这个问题或许不是那么重要。但我还计划测试期货的 CTA 策略。如果用这个数据回测策略，和实盘将有会很大差异，因为那些跳空是不同合约的价差，是实际交易无法拿到的收益。\n如何解决？\n答案就是计算复权数据，通过复权计算买入持有的收益曲线，将换仓时的价差尽可能的考虑在内。\n熟悉股票交易的人都知道，交易软件或数据平台会提供股票的复权价格（如分红、拆股后的调整数据），以消除公司行为导致的历史价格断裂，确保长期收益计算的准确性。\n但期货不同，交易软件和数据平台通常不会提供这个数据。\n为什么呢？\n所谓的主连合约是基于某个规则拼接而成，复权并非强制需求。根据策略目标不同，可自行选择换月规则，如固定日期、持仓量切换等。\n散户能接触到的数据源通常只提供主连合约的原始价格数据。听过付费平台（如 Wind）好像有定制的主连合约，也提供了相应的复权因子。\n那我这个小散该怎么办？\n当然是自己计算了。走独立自主的发展道路，还可以根据策略目标制定不同的换仓时间和复权计算的方法。\n计算期货复权数据 # 正式进入主题，本文我将介绍如何基于 tushare 计算主连合约的复权数据。\n核心有两部分：\n一是拿到主连合约与实际合约的映射； 二是计算复权因子； 准备工作 # 下载安装将用到的几个 Python 库。\npip install tushare pandas matplotlib click tqdm 这里用到了一个不常用的库 - tqdm，它是用来展示数据下载进度的，特别当下载数量非常大时，这个小技巧能提升你的体验甚至效率。\n简单示例：\nimport time from tqdm import tqdm for _ in tqdm(range(100)): time.sleep(0.1) 效果如下：\n100%|██████████████████████████| 100/100 [00:01\u0026lt;00:00, 86.14it/s] 导入所有库，配置认证 tushare 的 token。\nimport tushare as ts import pandas as pd from tqdm import tqdm pro = ts.pro_api(\u0026#34;你的 Tushare Pro Token\u0026#34;) 映射数据 # 映射数据从哪里拿？\n我可以直接从数据提供商那里获取。如果能明确换仓规则，也可自己计算，即我知道如主连合约的底层合约何时从一个换到另一个。\n标准映射数据 # 先说下如何拿到标准主连的映射关系，也就是如同花顺等交易软件上一样的主连合约规则，通过 tushare 的 fut_mapping 能直接拿到。我能接触到的数据源中，只在 tushare 看到了这个标准规则的映射关系。\n如获取生猪的主连合约从2022年1月1日到现在与底层合约的映射，示例代码：\nmapping_data = pro.fut_mapping( ts_code=\u0026#34;LH.DCE\u0026#34;, start_date=\u0026#34;20220101\u0026#34;, ) mapping_data.sort_values( by=\u0026#34;trade_date\u0026#34;, inplace=True, ignore_index=True, ) print(mapping_data) 输出：\nts_code trade_date mapping_ts_code 0 LH.DCE 20220104 LH2203.DCE 1 LH.DCE 20220105 LH2203.DCE 2 LH.DCE 20220106 LH2203.DCE 3 LH.DCE 20220107 LH2203.DCE 4 LH.DCE 20220110 LH2203.DCE .. ... ... ... 762 LH.DCE 20250303 LH2505.DCE 763 LH.DCE 20250304 LH2505.DCE 764 LH.DCE 20250305 LH2505.DCE 765 LH.DCE 20250306 LH2505.DCE 766 LH.DCE 20250307 LH2505.DCE 如上的 mappint_ts_code 主连合约当前的底层合约。\n我没有去深究这个规则，最常见的连续合约就是主力连续，主力合约似乎是以持仓为判断标准编织，还有其他的连续合约，如按固定日期切换，复权，最小价差等。\n假设，Tushare 还提供了一个生猪连续合约的映射关系，在 LH 后加上 L，即 LHL，它的代码规则是 主力合约代码+L。如果想要所有的合约信息，可通过 tushare 的 fut_basic 接口拿到合约的基础信息。\n自定义换仓规则 # 如果无法拿到这个标准的映射数据，可自定义一个尽量接近于标准的换仓规则。\n我定义了一个固定日期换仓规则：\n每月 28 号换仓到下一个合约，要求交割日期不少于 40 天。我将其封装成了一个函数，其中用到了 tushare 的 fut_basic 合约列表接口、trade_cal 交易日历接口。\n函数定义：\ndef fut_mapping(fut_code, exchange) 这是个简单例子，但在文章中介绍还是有点繁琐，就不展开了。请查看 custom_fut_mapping.py。\n调用示例：\nmapping_data = fut_mapping(\u0026#34;LH\u0026#34;, exchange=\u0026#34;DCE\u0026#34;) print(mapping_map) 输出：\ntrade_date ts_code mapping_ts_code 0 20210108 LHC LH2109.DCE 1 20210111 LHC LH2109.DCE 2 20210112 LHC LH2109.DCE 3 20210113 LHC LH2109.DCE 4 20210114 LHC LH2109.DCE ... ... ... ... 1001 20250303 LHC LH2507.DCE 1002 20250304 LHC LH2507.DCE 1003 20250305 LHC LH2507.DCE 1004 20250306 LHC LH2507.DCE 1005 20250307 LHC LH2507.DCE 其中的 LHC 是我随意定义的表示这个规则的生猪合约代码。\n不知道这个代码有没有参考价值。\n如果你没有途径拿到标准的主连合约的映射关系，这个或许可参考，将 tushare 替换为你的数据源即可。你可以修改规则，如按最大持仓，价差最小等等，计算这些规则所需的数据基本都是公开可获取的。\n查询换仓详情 # 如现在我想查看换仓详情，只要记录上个交易日的 mapping_ts_code，比较前后 mapping_ts_code 是否相同。\n# 之前的合约 mapping_data[\u0026#34;pre_mapping_ts_code\u0026#34;] = mapping_data[\u0026#34;mapping_ts_code\u0026#34;].shift(1) # 当前合约和之前合约不同且之前合约不为空 mapping_data[\u0026#34;rollover\u0026#34;] = ( (mapping_data[\u0026#34;mapping_ts_code\u0026#34;] != mapping_data[\u0026#34;pre_mapping_ts_code\u0026#34;]) \u0026amp; mapping_data[\u0026#34;pre_mapping_ts_code\u0026#34;].notna() ) print(mapping_data[mapping_data[\u0026#34;rollover\u0026#34;]][ [\u0026#34;ts_code\u0026#34;, \u0026#34;trade_date\u0026#34;, \u0026#34;mapping_ts_code\u0026#34;, \u0026#34;pre_mapping_ts_code] ]) 输出：\nts_code trade_date mapping_ts_code pre_mapping_ts_code 20 LH.DCE 20220208 LH2205.DCE LH2203.DCE 68 LH.DCE 20220419 LH2209.DCE LH2205.DCE 143 LH.DCE 20220808 LH2301.DCE LH2209.DCE .. ... ... ... 667 LH.DCE 20241010 LH2501.DCE LH2411.DCE 714 LH.DCE 20241216 LH2503.DCE LH2501.DCE 747 LH.DCE 20250210 LH2505.DCE LH2503.DCE 从上面就能看出来，合约是哪天（trade_date）从哪个合约（pre_mapping_ts_code）切换到当前的合约（mapping_ts_code）。\n计算复权数据 # 到此，已经拿到了最难获取的主连合约映射关系，开始计算复权数据。\n这块的核心是计算复权因子。有了复权因子，接着只要将复权因子作用到原始价格上即可拿到复权价格数据。\n复权方法 # 常见的复权因子公式有哪些？\nopen/pre_close，即当前合约的开盘价与上个合约收盘价的比值 pre_close/pre_close，即当前合约和上个合约收盘价的比值； pre_settle/pre_settle，即当前合约与上个合约结算价的比值； open/open，即当前合约和上个合约的开盘价的比值； 如上统一采用的是比值计算方法，只是计算所用的价格不同。这篇文章会介绍前三种方法的实现，第四种 open/open 在脚本也实现了，就不再文章里展开了。\n接下来具体介绍这四个复权因子的计算方法。\nopen/pre_close # open/pre_close 是最简单的复权因子计算方法，因为在矩阵计算中，这最容易实现，不少文章就写了这种实现。对应到实际操作，即在合约切换收盘日平仓，次日在新合约重新开仓即可。\n计算复权因子，要先拿到价格数据。\n如果你用的 tushare 作为数据源，直接调用 fut_daily 就能拿到主连的日线数据，通过 open/pre_close 即可。不过为了与接下来其他计算方式的统一，我还是通过调取实际合约的价格，将它们与主连合约重新拼接得到主连的价格。\n为了拿到实际合约的价格，我要知道每个合约在主连合约上出现的开始和结束时间。\ndef extract_date_range(group): return group[\u0026#34;trade_date\u0026#34;].min(), group[\u0026#34;trade_date\u0026#34;].max() date_ranges = mapping_data.groupby(\u0026#34;mapping_ts_code\u0026#34;).apply(extract_date_range) print(date_ranges) 输出：\nmapping_ts_code LH2109.DCE (20210108, 20210818) LH2201.DCE (20210819, 20211210) LH2203.DCE (20211213, 20220207) ... ... LH2501.DCE (20241010, 20241213) LH2503.DCE (20241216, 20250207) LH2505.DCE (20250210, 20250307) dtype: object 现在基于 ts_code 和 date_range 从 fut_daily 获取接口数据。\nall_ohlcvs = [] for ts_code, (start_date, end_date) in tqdm(date_ranges.items()): ohlcvs = pro.fut_daily(ts_code=ts_code, start_date=start_date, end_date=end_date) if not ohlcvs.empty: all_ohlcvs.append(ohlcvs) ohlcv_data = pd.concat(all_ohlcvs, ignore_index=True) 将 ohlcv_data mapping_data 合并就能得到主连合约完整的数据了。\ndata = mapping_data.merge( ohlcv_data, left_on=[\u0026#34;mapping_ts_code\u0026#34;, \u0026#34;trade_date\u0026#34;], right_on=[\u0026#34;ts_code\u0026#34;, \u0026#34;trade_date\u0026#34;], how=\u0026#34;left\u0026#34;, suffixes=(\u0026#34;\u0026#34;, \u0026#34;_1\u0026#34;), ) data.drop(columns=[\u0026#34;ts_code_1\u0026#34;], inplace=True) print(data[[\u0026#34;ts_code\u0026#34;, \u0026#34;trade_date\u0026#34;, \u0026#34;open\u0026#34;, \u0026#34;pre_close_before\u0026#34;]]) 输出：\nts_code trade_date open pre_close 0 LH.DCE 20220104 14320.0 14450.0 1 LH.DCE 20220105 14165.0 14165.0 2 LH.DCE 20220106 14250.0 14245.0 3 LH.DCE 20220107 14150.0 14075.0 4 LH.DCE 20220110 13670.0 13915.0 .. ... ... ... ... 762 LH.DCE 20250303 12950.0 12935.0 763 LH.DCE 20250304 13100.0 13125.0 764 LH.DCE 20250305 13160.0 13195.0 765 LH.DCE 20250306 13055.0 13070.0 766 LH.DCE 20250307 13100.0 13090.0 复权因子的计算是 open/pre_close 即可，如果是后复权的话，要取倒数，即 pre_close/open。\n这里还有有个小问题，默认的 pre_close 是当前映射合约上个交易日的收盘价，不是上个映射合约的上个交易日的收盘价。\n一行代码解决：\ndata[\u0026#34;pre_close_before\u0026#34;] = data[\u0026#34;close\u0026#34;].shift(1) 计算复权因子，以后复权为例。\n# 换仓时的数据 rollover_data = data[data[\u0026#34;rollover\u0026#34;]] # 计算复权因子 data[\u0026#34;rollover_factor\u0026#34;] = rollover_data[\u0026#34;open\u0026#34;] / rollover_data[\u0026#34;pre_close_before\u0026#34;] data[\u0026#34;adj_factor\u0026#34;] = (1/data[\u0026#34;rollover_factor\u0026#34;]).fillna(1).cumprod() 后复权的 adj_factor 是复权因子的倒数，这里要处理下。\n计算复权价格并绘制收盘价：\ndata[\u0026#34;adj_close\u0026#34;] = data[\u0026#34;close\u0026#34;] * data[\u0026#34;adj_factor\u0026#34;] data[[\u0026#34;adj_close\u0026#34;, \u0026#34;close\u0026#34;]].plot() 绘制价格图表：\ndata[\u0026#39;trade_date\u0026#39;] = pd.datetime(data[\u0026#39;trade_date\u0026#39;]) data.set_index(\u0026#34;trade_date\u0026#34;, inplace=True) data.index.name = \u0026#34;date\u0026#34; plt.show() 绘图如下：\n复权价格和实际的价格差异还是很大。\npre_close/pre_close 和 pre_settle/pre_settle # pre_close/pre_close 和 pre_settle/pre_settle 的复权因子的计算也很简单了。因为这里是手动拼接的价格数据，当前合约和上个合约的价格上个交易日的价格都很容易拿到。\npre_close/pre_close：\ndata[\u0026#34;rollover_factor\u0026#34;] = rollover_data[\u0026#34;prev_close\u0026#34;] / rollover_data[\u0026#34;pre_close_before\u0026#34;] data[\u0026#34;adj_factor\u0026#34;] = (1/data[\u0026#34;rollover_factor\u0026#34;]).fillna(1).cumprod() pre_settle/pre_settle：\ndata[\u0026#34;pre_settle_before\u0026#34;] = data[\u0026#34;settle\u0026#34;].shift(1) data[\u0026#34;rollover_factor\u0026#34;] = rollover_data[\u0026#34;prev_settle\u0026#34;] / rollover_data[\u0026#34;pre_settle_before\u0026#34;] data[\u0026#34;adj_factor\u0026#34;] = (1/data[\u0026#34;rollover_factor\u0026#34;]).fillna(1).cumprod() 有了复权因子，就可以将价格数据与其相乘就能拿到复权价格了。\n总结 # 这篇文章主要实现如何基于 tushare 计算期货的复权数据，包含了常见的复权计算方法，完整的脚本请查看地址：download_future_adj_data.py。如果没有 tushare 数据，文中也提供了实现的具体思路，即使没有合约映射数据，也可以自己制定规则实现。\n希望本文对你有用。\n最近有不少朋友希望我开群，如果有这个需求的，可以扫码加群：\n补充：\n如果想直接用 Tushare 的数据，可联系我，有 20% 的折扣。Tushare 一年 200 元的积分等级就包含了这篇文章中的所有数据 API（这个等级其实已经支持了 tushare.pro 上 60% 的数据接口）。如只是临时需要，可以免费提供一个品种的复权数据。\n","date":"2025-03-08","externalUrl":null,"permalink":"/posts/2025-03-08-adj-data-using-tushare-in-china-future-market/","section":"文章","summary":"本周写点国内期货的内容，介绍如何通过 Python 计算期货主连合约的复权数据。之所以要处理这个数据，是因为它会影响策略（如 CTA 策略）的回测结，是个大坑。新手要理解下期货与股票的差异。\n","title":"期货回测避坑-基于 tushare 计算期货复权价格","type":"posts"},{"content":"前两周发了一篇关于如何监控加密货币跨市场套利监控的文章，介绍了基础的实现思路和示例代码。有不少朋友对这个内容表现出了浓厚的兴趣，也有人想要这样一个监控工具。\n上周我将示例代码重新整合，开发了一个终端监控工具，也修复了之前示例中的一些 bug，。如果还不是了解的，查看前文 实时监控加密货币跨交易所的套利机会。\n使用示例 # 接下来，演示下在不同市场间的监控案例。提示：可使用 Ctrl+q 退出监控终端。\n基于最新成交（Binance 现货和 OKX 正向永续合约的全量监控)：\n$ python main.py --monitor-panel ticker \\ --market-a binance.spot \\ --market-b okx.swap.linear 如上所示，默认展示的 USDT 合约，如果要切换成 USDC，可通过选项 --quote-currency USDC 实现。\n而排序规则上，固定按价差百分比从大到小排序。\n$ python main.py --monitor-panel ticker \\ --market-a binance.spot \\ --market-b okx.swap.linear \\ --quote-currency USDC 如果是反向合约，如 binance.swap.invese，计价币种要切换成 USD，配置选项 --quote-currency USD。\n当前版本的这个工具虽然是通过 asyncio 异步 IO 实现，但单进程的工具。全量监控时，计算速度慢，实时性影响较大。如上图所示，延迟在秒级别。建议加上 \u0026ndash;symbols 来明确监控范围，如 --symbols BTC-USDT,ETH-USDT,XRP-USDT,TRUMP-USDT。\n$ python main.py --monitor-panel ticker \\ --market-a binance.spot \\ --market-b okx.swap.linear \\ --symbols BTC-USDT,ETH-USDT 除了 ticker 价差监控，这个工具还实现了订单簿的价差监控，如下所示：\n$ python main.py --monitor-panel orderbook \\ --market-a binance.spot \\ --market-b okx.swap.linear \\ --symbols TRUMP-USDT,BTC-USDT,ETH-USDT,SOL-USDT,ADA-USDT,BNB-USDT,XRP-USDT 通过 --monitor-panel 切换监控类型为通过 orderbook 订单薄计算价差。\n这个价差是基于挂单最佳买卖单计算而来。\n挂单的消息数据量比 ticker 数据要高，如果全量监控的话，延迟能到 10秒以上的延迟。\n源码下载 # 我已经将这个工具的代码放在了 Github 上，通过 Git 即可下载代码仓库。\n命令如下：\n$ git clone https://github.com/poloxue/seekopt $ cd seekopt $ pip install -r requirements.txt 说实话，我没有测试过 requirements.txt 中的依赖是否完整，如果遇到问题，可以在仓库 issue 中说明，或者加我微信。\nweb 服务 # 这个监控工具的界面用的是 textual 开发的，textual 是一个 Tui 终端应用开发库。我暂时还不想整 Web 开发，就先用它实现了。\n如果有意将其通过 web 访问，textual 也有命令可将其作为 web 服务。\n$ textual serve \u0026#34;python main.py \\ --market-a binance.swap.linear \\ --market-b bybit.swap.linear\u0026#34; ___ ____ _ _ ___ _ _ ____ _ ____ ____ ____ _ _ ____ | |___ \\/ | | | |__| | __ [__ |___ |__/ | | |___ | |___ _/\\_ | |__| | | |___ ___] |___ | \\ \\/ |___ v1.1.1 Serving \u0026#39;python main.py --market-a binance.swap.linear --market-b bybit.swap.linear\u0026#39; on http://localhost:8000 Press Ctrl+C to quit 不过有个问题，这种方式提供的 web 服务，如果是多人使用，好像不是共享同一个监控脚本，这会导致资源的重复占用。\n适用市场 # 这个工具是基于 ccxt 实现，按理说 ccxt 支持的市场都是支持的，不过我测试的时候，也发现了一些不适配的情况。我当前只测试了 binance、okx 和 bybit 三个交易所。\n参数 market 格式是 exchange.type.subtype，支持类型如下所示：\nexchange.spot -\u0026gt; 现货 exchange.spot.margin -\u0026gt; 保证金杠杆 exchange.swap.linear -\u0026gt; 永续正向合约 exchange.swap.inverse -\u0026gt; 永续反向合约 exchagne.future.linear -\u0026gt; 交割正向合约 exchagne.future.inverse -\u0026gt; 交割反向合约 使用时，请把 exchange 替换为具体的交易所名称。\n实时性 # 如果全量监控所有交易对，延迟会比较大，建议指定 symbols 限制监听的交易对数量。如果想提高实时性，最简单的方案多启动几个程序，按批次监听不同的交易列表。\n我现在不确定这个工具是否确有需求，如果确有需求，我后续会考虑加入多进程和分布式能力，提升这个工具的实时性能。\n延迟计算 # 关于这个监控工具上的实时性数值，是在计算价差时的时间减去消息上的时间戳得到。\n这里有个问题，本地时钟和服务器时钟可能是不一样的。为了解决这个问题，程序会定时同步交易所的 servertime，计算与本地时间的偏差保持两边的同步，但获取服务器时间也有网络延迟，虽然说有粗略的方法计算这个延迟，但如果网络不好，有可能看到延迟时间是负数。\n另外的方案，或许可以设置机器同步的 ntp server，保持和交易所时钟同步。这个或许可以尝试下。不过我没找到交易所用的是什么 ntp 服务器。\n总结 # 这个价差监控工具还只是个初版实现。如果使用时遇到的什么问题或时有什么想法，请提出来，我会继续优化改进。\n最后，希望本文对你有用。\n","date":"2025-03-03","externalUrl":null,"permalink":"/posts/2025-03-03-crypto-spread-monitor-using-python/","section":"文章","summary":"前两周发了一篇关于如何监控加密货币跨市场套利监控的文章，介绍了基础的实现思路和示例代码。有不少朋友对这个内容表现出了浓厚的兴趣，也有人想要这样一个监控工具。\n","title":"开发了一个加密货币跨市场价差监控工具","type":"posts"},{"content":"最近研究了一个叫 Hummingbot 的 Python 加密货币机器人，它提供了一些常见的高频策略，如高频做市、跨市场套利和资金费率套利等。我很早就知道 Hummingbot，但一直没有测试它，主要是它用起来不舒服，代码不是很清晰。\nHummingbot 是一款高频量化机器人，里面有大量 C++ 代码，还有基本都是 asyncio 异步操作。我决定搞 Hummingbot 也是因为它内置了一些高频做市、跨市场套利、资金费率套利和 cex/dex 套利交易等策略，作为高频交易的学习材料，还是不错的。还有，最近发现它还支持 CEX/DEX 套利，那无论如何也要搞下它了。\n事件机制的话，Hummingbot 中的 on_tick 不是由成交 tick 触发的，而是定时的 clock tick，如配置 1 秒、1分钟定时触发。更细粒度的时间控制，最小可设置到 0.1s。这个和 vnpy 不同，我好像记得之前有人吐槽 vn.py 不支持定时触发的能力，因此更喜欢 backtrader。\nHummingbot 的数据源基本都是 ws 实时监听得到，频繁的的获取数据，如价格、订单簿（OrderBook），不会触发交易所的频率限制。当然，下单是例外，这是主动触发的操作。\n本文尝试走一遍流程，跑一个它的内置做市机器人试试。\n交互模式 # Hummingbot 提供了两种交互模式 Tui 终端和 Web 端的 Dashboard。早期版本只提供了 TUI 的终端交互模式，完全是通过命令行管理的机器人。\n2023 年上线 Web 端的 Dashboard，提供了 Web 端管理机器人的能力。Dashboard 是基于 streamlit 实现的。前段时间，我还用 streamlit 做了个简单的 A 股股票筛选器 demo，有兴趣移步到 使用 Streamlit 打造一个股票筛选分析工具。\n本文，我将重点通过 Tui 终端模式演示如何运行 Hummingbot 的机器人。\n安装启动 # Hummingbot 的安装可通过源码或是 docker。如果不是要阅读源码或调试问题，建议是用 docker 安装，毕竟简单省力，把更多精力到核心功能上。当然，源码安装也不复杂，但要有好的网络。\n注：开始前，请确保你已经安装 Docker 了。\n下载代码仓库 # 使用 git 下载 Hummingbot 的代码仓库。\ngit clone https://github.com/hummingbot/hummingbot.git 我在下载的时候常遇到网络 Timeout，可通过 \u0026ndash;depth=1 只下载最新的文件，提升下载速度。\ngit clone --depth=1 https://github.com/hummingbot/hummingbot.git 启动应用 # 进入 hummingbot 目录通过 docker 启动服务：\ndocker compose up -d 现在，用 docker attach 命令就能进到应用入口：\ndocker attach hummingbot 你会看到如下画面。\n第一次启动，会要求你设置登陆密码，以后的每次进入 Hummingbot 终端都离不开这个密码。\n补充一点，exit 退出交互模式时，整个服务都会停止，要重新 docker-compose up -d 启动，docker attach 重新登陆。如果想退出但保持运行状态，可通过 Ctrl+p + Ctrl+q 快捷键组合（保持 Ctrl 按住状态）即可退出。\n查看帮助信息 # 成功登陆后，你将会进入到一个 Hummingbot 的终端，如果想查看它支持什么命令，输入 help 查看它支持的命令，这些命令提供了在终端管理机器人的能力。\n\u0026gt; help usage: {connect,create,import,help,balance,config,start,stop,status,history,gateway ,exit,export,ticker,previous,mqtt,spreads,rate,order_book,tab_example} ... positional arguments: {connect,create,import,help,balance,config,start,stop,status,history,gateway,exit ,export,ticker,previous,mqtt,spreads,rate,order_book,tab_example} connect List available exchanges and add API keys to them create Create a new bot import Import an existing bot by loading the configuration file help List available commands balance Display your asset balances across all connected exchanges config Display the current bot\u0026#39;s configuration start Start the current bot stop Stop the current bot status Get the market status of the current bot history See the past performance of the current bot gateway Helper comands for Gateway server. exit Exit and cancel all outstanding orders export Export secure information ticker Show market ticker of current order book previous Imports the last strategy used mqtt Manage MQTT Bridge to Message brokers spreads Set bid and ask spread rate Show rate of a given trading pair order_book Display current order book tab_example Display hello world 我将重点用到几个命令：\nconnect，用于配置交易所连接器； balance，查询账户余额； create，创建机器人配置； start，运行机器人； stop，停止当前的机器人； status，监控机器人运行状态； history，查看机器人的历史表现； config，配置全局参数； 到此，就完成了 Hummingbot 基础安装。\n实现步骤 # 正式开始前，先介绍下大概的实现流程，主要分为如下的四个步骤。\n连接交易所，介绍如何配置和连接交易所，查看账号的信息，如当前的账户余额； 配置启动策略，介绍配置并成功一个 Hummingbot 内置的简单做市的策略机器人； 监控运行状态，介绍如何监控运行中机器人的状态，如订单、成交、盈亏和行情等； 接下来，开始正式行动吧！\n连接交易所 # 这部分会涉及两个执行步骤。首先要确认用什么交易所，其次是配置交易所的认证信息和连接。\n选择交易所 # 选择交易所前，先看下 Hummingbot 支持的交易所列表吧。输入 connect，列出可用的交易所，输出如下：\n\u0026gt; connect Testing connections, please wait... +---------------------------+------------+-----------------+ | Exchange | Keys Added | Keys Confirmed | |---------------------------+------------+-----------------| | binance | No | No | | binance_perpetual | No | No | | binance_perpetual_testnet | Yes | Yes | | binance_us | No | No | | bitget_perpetual | No | No | | bybit | No | No | | bybit_perpetual | No | No | | bybit_perpetual_testnet | No | No | | bybit_testnet | No | No | | dydx_v4_perpetual | No | No | | okx | No | No | | okx_perpetual | No | No | +---------------------------+------------+-----------------+ 这里我只保留了一些常用的交易所，你如果执行命令会看到更多的选项。\n从上面可知，我已经设置了 Binance 合约的 testnet 环境，所以 binance_perpetual_testnet 的 Key Added 和 Keys Confirmed 都是 Yes。其他如常用的 okx 和 binance 是都是支持的，那就是它们了。\n连接器名称上 exchange 和 exchange_perpetual 的区别就是现货和合约模式的区别。如果希望支持做空，就用支持合约的交易连接器，OKX 就是 okx_perpetual，而 Binance 就是 binance_perpetual。否则就是 okx 和 binance。\n配置交易所 # 接下来开始配置这两个连接器，要拿到 API 的认证信息，如 API_KEY API_SECRET，OKX 还配置 PASSPHRASE。拿到 API 认证信息后，通过 connect exchange 即可将认证信息配置到系统。\n如下命令：\n\u0026gt; connect okx \u0026gt; connect okx_perpetual \u0026gt; connect binance_perpetual 按照 Hummingbot 终端的的交互提示配置就行了，一点也不复杂。\n这里吐槽两句，我本来想用 bybit 来测试的，在添加认证信息时，一直提示错误，查看 Hummingbot 的官方 issue，发现似乎修复代码还在 development 分支。\n特别提醒，Hummingbot 支持的是基础账户模式，如果升级了统一账户，有可能就不支持了。Binance 可通过开子账号解决这个问题的，OKX 的话， 将账户模式切换回基础模式下的 \u0026ldquo;现货合约模式\u0026rdquo; 即可。\n查询余额 # 连接成功后，可以查下账户的余额信息，看看是不是真的可用状态。\n\u0026gt; balance Updating balances, please wait... binance_perpetual: Asset Total Total ($) Allocated USDT 27.1375 27.13 0% Total: $ 27.13 Allocated: 0.00% okx: Asset Total Total ($) Allocated USDT 29.9658 29.96 0% Total: $ 29.96 Allocated: 0.00% okx_perpetual: Asset Total Total ($) Allocated USDT 29.9658 29.96 0% Total: $ 29.96 Allocated: 0.00% Exchanges Total: $ 87 接下来开始启动一个真正的交易机器人了。\n配置运行策略 # 配置和运行策略这部分的花了我不少时间，有些问题还是要阅读代码才能确认问题所在。\n这里虽然只跑了一个策略，但其实为了把它的架构搞清楚，测试了好几个策略。\n策略模版 # Hummingbot 默认提供了一些策略模版，执行 create --script-config 会显示可用模版。\ncreate --script-config [如下是输入补全提示] simple_pmm simple_vwap simple_xemm v2_directional_rsi v2_funding_rate_arb v2_twap_multiple_pairs v2_with_controllers v2_xemm 如下这几个常见策略的简单说明:\n基础做市（simple_pmm）：通过持续挂单捕捉买卖价差，适用于主流交易对的流动性维护。 跨交易所做市（simple_xemm/v2_xemm）：在一家交易所挂单，另一家对冲风险，利用交易所间价差获利。 时间加权（v2_twap_multiple_pairs）：将大额订单拆分为多笔小额订单，按时间均匀执行以减少市场冲击，支持多交易对。 成交量加权（simple_vwap）：根据市场实时成交量动态调整订单规模，降低交易滑点。 方向性 RSI（v2_directional_rsi）：基于 RSI 指标判断超买超卖信号，触发趋势跟踪交易。 资金费率套利（v2_funding_rate_arb）：通过永续合约与现货间的资金费率差异进行对冲套利。 控制器增强（v2_with_controllers）：这个比较复杂，可理解为总控管理多个子策略，总控负责风险控制、组合管理等。 我就那个 simple_pmm 先演示如何在 Hummingbot 中创建一个机器人吧！\n配置策略 # 在 simple_pmm 配置时，有两个注意点：\n首先，我在 okx_perpetual 上测试 simple_pmm 这个做市策略，发现它只适用于现货，不支持合约。\n还有，simple_pmm 是在订单簿上下挂单，而现货不同于合约，保证交易对两个币种钱包都有钱才能成功挂单。\n有了这些前提，开始尝试具体的配置吧！\n首先，通过 create --script-config 命令配置脚本参数，按交互式提示输入你的参数即可。\n\u0026gt;\u0026gt;\u0026gt; create --script-config simple_pmm Exchange where the bot will trade \u0026gt;\u0026gt;\u0026gt; okx Trading pair in which the bot will place orders \u0026gt;\u0026gt;\u0026gt; SOL-USDT Order amount (denominated in base asset) \u0026gt;\u0026gt;\u0026gt; 0.01 Bid order spread (in percent) \u0026gt;\u0026gt;\u0026gt; 0.003 Ask order spread (in percent) \u0026gt;\u0026gt;\u0026gt; 0.003 Order refresh time (in seconds) \u0026gt;\u0026gt;\u0026gt; 100 Price type to use (mid or last) \u0026gt;\u0026gt;\u0026gt; mid Enter a new file name for your configuration \u0026gt;\u0026gt;\u0026gt; conf_simple_pmm_sol.yml A new config file has been created: conf_simple_pmm_sol.yml 这个配置的意思是，在 order book 中间价上下 0.3% 的位置分别挂上 0.01 SOL 的买卖订单，每 60 秒会刷新重新下单。\n最后，将这个参数保存到命名为 conf_simple_pmm_sol.yml 的配置文件中。\n补充一点，如果在配置参数时，遇到配置错误，希望重新配置，可通过 Ctrl+x 退出参数配置模式，重新进入。\n启动策略 # 通过 start 启动这个策略机器人，格式是 start --script 策略脚本 --conf 策略配置文件，即用什么参数运行什么策略。\n命令如下：\n\u0026gt; start --script simple_pmm.py --conf conf_simple_pmm_sol.yml 如果一切配置正常，查看你的活跃订单，就能看到成功下了两个订单了。\n或者执行 status 命令，查看机器人当前的运行状态：\n\u0026gt; status Balances: Exchange Asset Total Balance Available Balance okx SOL 0.079768 0.079768 okx USDT 18.86025163 17.49605163 Orders: Exchange Market Side Price Amount Age okx SOL-USDT buy 136.42161 0.01 00:00:38 如果加上 --live 选项，可实时监控机器人。\n策略表现 # 如果想查看策略的表现，执行 history 查看，输出如下：\n\u0026gt; history Start Time: 2025-02-25 14:26:14 Current Time: 2025-02-25 15:24:17 Duration: 0 days 00:58:02 okx / SOL-USDT Trades: buy sell total Number of trades 13 19 32 Total trade volume (SOL) 0.1300 -0.1900 -0.0600 Total trade volume (USDT) -17.80 26.06 8.26 Avg price 136.9 137.1 137.0 Assets: start current change SOL 0.0997 0.0397 -0.0600 USDT 16.09 24.35 8.26 SOL-USDT price 138.4 136.3 -2.07 Base asset % 46.19% 18.20% -27.98% Performance: Hold portfolio value 29.69 USDT Current portfolio value 29.77 USDT Trade P\u0026amp;L 0.0774 USDT Fees paid 0.0208 USDT Fees paid 0.00010 SOL Total P\u0026amp;L 0.0423 USDT Return % 0.14% 我在一边写文章，一边测试这个策略，正好赶上 SOL 的大波动，跑了 1 小时还赚了一点。\n其他一些命令 # stop，停止这个机器人； order_book，查看当前的 order_book（右侧 Pane）,可加上 --live 实时监控； ticker，查看当前的价格，也支持 --live 选项； import，导入策略配置，看起来是用于导入老版的策略配置； 总结 # 我的感受，Hummingbot 相对其它的框架，会复杂一点，特别是开发一个新策略时。它也在不断迭代，基本每一两个月就有一个新版本发布。这篇文章只是简单演示了下它的使用。\nHummingbot 还有很多内置策略，除了演示的这个 simple_pmm 简单做市外，还有如跨交易所做市套利、资金费率套利、CEX/DEX 套利、网格、DCA等等。如果要做 DEX/CEX 的套利，配置的内容也挺多的。\n这篇文章也没有介绍如何自己开发一个 Hummingbot 机器人，还有它的新版模块化架构，如何管理子策略。本来想多整理点的，但不想文章太长了。就到先到这里，后续慢慢来吧。\n最后，希望本文对你有用！\n","date":"2025-02-22","externalUrl":null,"permalink":"/posts/2025-02-22-cross-exchange-arbitrage-using-humming/","section":"文章","summary":"最近研究了一个叫 Hummingbot 的 Python 加密货币机器人，它提供了一些常见的高频策略，如高频做市、跨市场套利和资金费率套利等。我很早就知道 Hummingbot，但一直没有测试它，主要是它用起来不舒服，代码不是很清晰。\n","title":"通过 Hummingbot 运行一个做市机器人","type":"posts"},{"content":"前面写了一篇关于如何用 Python 获取数字货币行情的博文，有评论希望我展开来说说跨交易所的套利机会。一直觉得随着参与这类策略的人多了，机会很少了，就没有深入研究过。这次借这个机会尝试一下。\n本文是我的一次尝试，将介绍如何用 python 实现一个跨交易所套利机会的监控工具。\n免责声明：本文内容仅供学习研究使用，不构成任何投资或交易建议。实际应用前请自行评估风险，作者不对由此产生的损失承担责任。\n是什么？ # 什么是跨交易所的价差套利？跨交易所套利是通过捕捉同一资产在不同交易所的价格差异实现低风险收益的策略。\n例如，当 BTC 在 Binance 价格为 90000，而在 OKX 为 90200 时，就可以在 Binance 低价买入并在 OKX 高价卖出。当它们的价格重新回归到相等时，平掉仓位，即可得到价差带来的收益。\n价差套利的分类有很多，如期现套利、跨期套利、跨品种套利，价差套利其实就是将相关性的品种进行对冲交易。这些套利策略中，有些是低风险套利，而有些可能存在较大风险，不同品种间的相关性有可能失效，或者短期出现超越风险承受力的大幅度波动。\n本文介绍是加密货币跨交易所套利，尝试开发一个工具监控不同交易所间的相同币种的实时价格发现潜在的套利机会。\n再次提示：这是我的一次尝试而已，和金融交易相关的内容还是要小心。\n实现路径 # 在开发这个工具前，我要先明确工具实现的流程，大概分几个步骤。\n筛选出跨交易所间可配对品种，且要过滤或修正一些错配； 监听筛选出的品种的最近价格，计算其配对价差； 从应用角度分析，如何将这个监控结果运用于实际交易中； 开始上手实操吧！\n配对品种 # 开始第一步部分，写如何将不同交易所的交易对匹配起来。\n如何识别呢？\n我将通过代码拉取两个交易所的交易对，按计价和基准币种配对，如 BTC/USDT，计价货币是 USDT、基准货币是 BTC。\n还有，配对时要明确品种类型，避免不符合预期的配对，如希望配对交易所 A 和 B 的永续合约 ，但配对的却是 A 交易所的现货和 B 交易所的永续合约。\nCEX 的品种类型，有主类型和子类型的区别，主类型可能就是现货 spot、永续合约 swap、交割合约 future、期权 option，而子类型如对合约，无论是永续还是交割，都有正向合约（U本位）、反向合约（币币本位）。这块我们不用考虑 option，它和其他交易品种差异比较大。\n如果没接触这部分内容，可能难以理解的，其实画一张关系图画，就容易理解了。但是我有点懒。\n我希望这个匹配的函数的定义如下：\ndef load_pairs( exchange_a, # 交易所 a 实例 exchange_b, # 交易所 b 实例 type_a = \u0026#34;spot\u0026#34;, # 品种 a 主类型 subtype_a = None, # 品种 a 子类型 type_b = \u0026#34;spot\u0026#34;, # 品种 b 主类型 subtype_b = None, # 品种 b 子类型 ) 详细的实现代码，如下所示：\nimport os import ccxt params = { \u0026#39;enableRateLimit\u0026#39;: True, } def load_pairs( exchange_a, exchange_b, type_a = \u0026#34;spot\u0026#34;, subtype_a = None, type_b = \u0026#34;spot\u0026#34;, subtype_b = None, ): exchange_a.load_markets() exchange_b.load_markets() markets_a = { (m[\u0026#39;base\u0026#39;], m[\u0026#39;quote\u0026#39;]): m[\u0026#39;symbol\u0026#39;] for m in exchange_a.markets.values() if m[\u0026#39;type\u0026#39;] == type_a and (subtype_a is None or m[subtype_a]) } markets_b = { (m[\u0026#39;base\u0026#39;], m[\u0026#39;quote\u0026#39;]): m[\u0026#39;symbol\u0026#39;] for m in exchange_b.markets.values() if m[\u0026#39;type\u0026#39;] == type_b and (subtype_b is None or [subtype_b]) } pair_keys = set(markets_a.keys()).intersection(set(markets_b.keys())) return [ { \u0026#39;base\u0026#39;: base, \u0026#39;quote\u0026#39;: quote, \u0026#39;symbol_a\u0026#39;: markets_a[(base, quote)], \u0026#39;symbol_b\u0026#39;: markets_b[(base, quote)], } for base, quote in pair_keys ] 如下示例代码，将不同交易所的交易对关联起来的。\nbinance = ccxt.binance(params) okx = ccxt.okx(params) # Binance 现货对 OKX 现货 pairs = load_pairs(binance, okx, type_a=\u0026#39;spot\u0026#39;, type_b=\u0026#39;spot\u0026#39;) # Binance 现货对 OKX 正向永续合约 pairs = load_pairs(binance, okx, type_a=\u0026#39;spot\u0026#39;, type_b=\u0026#39;swap\u0026#39;, subtype_b=\u0026#39;linear\u0026#39;) 同一个交易所的不同类型品种当然也是可以配对的。\n# 初始化交易所对象 okx = ccxt.okx(params) # OKX 现货对 OKX 正向永续合约 pairs = load_pairs(okx, okx, type_a=\u0026#39;spot\u0026#39;, type_b=\u0026#39;swap\u0026#39;, subtype_b=\u0026#39;linear\u0026#39;) 不过即使按照上述的规则，依然会出现错配。为什么？\n因为不同交易所可能出现同名不同币的情况，如 NEIRO 在 OKX 和 Bybit 就是不同币种，OKX 上有两个 NEIRO，其中和 Bybit 配对的是 NEIROETHUSDT 合约。具体原因就不介绍了。\n为了防止这类情况，可以将价差异常的配对找出，人工确认原因。\n尝试实现这个代码：\ndef detect_abnormal_pairs(exchange_a, exchange_b, pairs, threshold = 0.05): \u0026#34;\u0026#34;\u0026#34; 用于检测价差异常的函数 参数说明： :param matched_pairs: load_pairs函数返回的匹配交易对列表 :param threshold: 视为异常的价格差异比例（0.05表示5%） 返回结构： { \u0026#39;base\u0026#39;: 基准货币, \u0026#39;quote\u0026#39;: 计价货币, \u0026#39;symbol_a\u0026#39;: 交易所A交易对, \u0026#39;symbol_b\u0026#39;: 交易所B交易对, \u0026#39;price_a\u0026#39;: 原始价格A, \u0026#39;price_b\u0026#39;: 原始价格B, \u0026#39;spread_pct\u0026#39;: 价差比例, \u0026#39;is_abnormal\u0026#39;: 是否异常 } \u0026#34;\u0026#34;\u0026#34; normal_pairs = [] abonormal_pairs = [] for pair in pairs: try: # 获取最新行情数据（单次尝试） ticker_a = exchange_a.fetch_ticker(pair[\u0026#39;symbol_a\u0026#39;]) ticker_b = exchange_b.fetch_ticker(pair[\u0026#39;symbol_b\u0026#39;]) # 获取最后成交价 price_a = ticker_a.get(\u0026#39;last\u0026#39;) price_b = ticker_b.get(\u0026#39;last\u0026#39;) # 跳过无效价格 if None in [price_a, price_b]: print(f\u0026#34;价格缺失: {pair[\u0026#39;symbol_a\u0026#39;]}/{pair[\u0026#39;symbol_b\u0026#39;]}\u0026#34;) continue # 计算价差比例（基于较小价格） min_price = min(price_a, price_b) spread_pct = abs(price_a - price_b) / min_price # 构建结果对象 result = { **pair, \u0026#39;price_a\u0026#39;: price_a, \u0026#39;price_b\u0026#39;: price_b, \u0026#39;spread_pct\u0026#39;: spread_pct, \u0026#39;is_abnormal\u0026#39;: spread_pct \u0026gt; threshold } if result[\u0026#39;is_abnormal\u0026#39;]: abonormal_pairs.append(result) else: normal_pairs.append(pair) except Exception as e: print(f\u0026#34;处理交易对 {pair} 时发生错误: {str(e)}\u0026#34;) return abonormal_pairs, normal_pairs 为了以防万一，异常的阈值可设置的小一些，如 0.05 即可。不过，期现的价差是有可能大于这个值的。\n如下代码将匹配交易对保存到 csv 文件。\nbinance = ccxt.binance(params) okx = ccxt.okx(params) pairs = load_pairs(binance, okx, \u0026#39;spot\u0026#39;, \u0026#39;future\u0026#39;) abnormal_pairs, normal_pairs = detect_abnormal_pairs(binance, okx, pairs, 0.05) pd.DataFrame(normal_pairs).to_csv(\u0026#34;normal_pairs.csv\u0026#34;) pd.DataFrame(abnormal_pairs).to_csv(\u0026#34;abnormal_pairs.csv\u0026#34;) 得到这两个 csv 文件后，建议手动接入检查文件中的配对情况，确认无误。\n还有一个坑，就算是相同币种，这个脚本也可能漏掉它们，如 PEPE 永续合约，在 OKX 的价格和 Binance 是不同的，因为 Binance 对 PEPE 放大了 1000 倍，名为 1000PEPEUSDT，基准货币是 1000PEPE，而不是 PEPE。这个特殊逻辑我就不实现了，有兴趣自行研究。通常都是一些 meme 币会出现这种情况。\n当然，如果你不是想全量监控，而是清楚想监控的是什么配对交易对，就无需全量匹配了。\n实时监控价差 # 接下来就是要监控它们的价差了。为了实时监控，还是通过 ccxt.pro 的 watch_ticker 方法实时监听最新成交价计算价差。\n最简单的方式就是顺序监控。\nticker_a = await exchange_a.watch_ticker(symbol_a) ticker_b = await exchange_b.watch_ticker(symbol_b) spread_pct = abs(ticker_a[\u0026#39;last\u0026#39;] - ticker[\u0026#39;last\u0026#39;])/min(ticker_b[\u0026#39;last\u0026#39;]) 不过为了更加实时，去掉两者等待的依赖，我用 watch_tickers 批量监听交易所 a 和 b 的所有 symbol，监听到新的价格就立刻计算价差，提高监听的性能。\n稍微有点复杂，不想展开了说明，直接看代码吧。\nimport os import asyncio import ccxt.pro as ccxtpro import pandas as pd import traceback from typing import Dict, Tuple from collections import defaultdict params = { \u0026#39;enableRateLimit\u0026#39;: True, # \u0026#39;proxies\u0026#39;: { # \u0026#39;http\u0026#39;: os.getenv(\u0026#39;http_proxy\u0026#39;), # \u0026#39;https\u0026#39;: os.getenv(\u0026#39;https_proxy\u0026#39;), # }, # \u0026#39;aiohttp_proxy\u0026#39;: os.getenv(\u0026#39;http_proxy\u0026#39;), # \u0026#39;ws_proxy\u0026#39;: os.getenv(\u0026#39;http_proxy\u0026#39;) } class Monitor: def __init__(self, exchange_a, exchange_b, pairs): self.exchange_a = exchange_a self.exchange_b = exchange_b # 构建symbol映射关系 self.symbol_map = self._build_symbol_map(pairs) self.pair_data: Dict[Tuple[str, str], dict] = {} self.monitor_tasks = [] self.running = False def _build_symbol_map(self, pairs): \u0026#34;\u0026#34;\u0026#34;构建symbol到配对关系的快速映射\u0026#34;\u0026#34;\u0026#34; symbol_map = defaultdict(dict) for pair in pairs: key = (pair[\u0026#39;base\u0026#39;], pair[\u0026#39;quote\u0026#39;]) symbol_map[\u0026#39;a\u0026#39;][pair[\u0026#39;symbol_a\u0026#39;]] = {\u0026#39;index\u0026#39;: \u0026#39;a\u0026#39;, \u0026#39;pair_key\u0026#39;: key} symbol_map[\u0026#39;b\u0026#39;][pair[\u0026#39;symbol_b\u0026#39;]] = {\u0026#39;index\u0026#39;: \u0026#39;b\u0026#39;, \u0026#39;pair_key\u0026#39;: key} return symbol_map async def monitor(self, exchange, index: str): \u0026#34;\u0026#34;\u0026#34; 统一监控方法 :param exchange: 交易所实例 :param index: 来源索引 (\u0026#39;a\u0026#39;或\u0026#39;b\u0026#39;) \u0026#34;\u0026#34;\u0026#34; symbols = [s for s in self.symbol_map[index]] while self.running: try: tickers = await exchange.watch_tickers(symbols) await self.process_tickers(tickers, index) except Exception as e: traceback.print_exc() print(f\u0026#34;监控异常({index}): {str(e)}\u0026#34;) await asyncio.sleep(5) async def process_tickers(self, tickers, index): \u0026#34;\u0026#34;\u0026#34;处理批量ticker数据\u0026#34;\u0026#34;\u0026#34; for symbol, ticker in tickers.items(): if symbol not in self.symbol_map[index]: continue pair_map = self.symbol_map[index][symbol] pair_key = pair_map[\u0026#39;pair_key\u0026#39;] # 初始化数据结构 if pair_key not in self.pair_data: self.pair_data[pair_key] = { \u0026#39;price_a\u0026#39;: None, \u0026#39;price_b\u0026#39;: None, \u0026#39;spread\u0026#39;: None } price_field = f\u0026#39;price_{index}\u0026#39; self.pair_data[pair_key][price_field] = ticker[\u0026#39;last\u0026#39;] # 立即计算价差 await self.calculate_spread(pair_key) async def calculate_spread(self, pair_key): \u0026#34;\u0026#34;\u0026#34;带校验的价差计算\u0026#34;\u0026#34;\u0026#34; data = self.pair_data[pair_key] try: if data[\u0026#39;price_a\u0026#39;] and data[\u0026#39;price_b\u0026#39;]: min_price = min(data[\u0026#39;price_a\u0026#39;], data[\u0026#39;price_b\u0026#39;]) spread_pct = abs(data[\u0026#39;price_a\u0026#39;] - data[\u0026#39;price_b\u0026#39;]) / min_price data[\u0026#39;spread_pct\u0026#39;] = spread_pct # 触发报警的价差阈值 if spread \u0026gt; 0.01: await self.trigger_arbitrage(pair_key) except (TypeError, ZeroDivisionError) as e: print(f\u0026#34;价差计算错误 {pair_key}: {str(e)}\u0026#34;) async def trigger_arbitrage(self, pair_key): \u0026#34;\u0026#34;\u0026#34;触发套利逻辑\u0026#34;\u0026#34;\u0026#34; data = self.pair_data[pair_key] print(f\u0026#34;套利机会! {pair_key} 价差: {data[\u0026#39;spread\u0026#39;]:.2%}\u0026#34;) # 此处添加实际交易逻辑 def start(self): \u0026#34;\u0026#34;\u0026#34;启动监控任务\u0026#34;\u0026#34;\u0026#34; self.running = True self.monitor_tasks = [ asyncio.create_task(self.monitor(self.exchange_a, \u0026#39;a\u0026#39;)), asyncio.create_task(self.monitor(self.exchange_b, \u0026#39;b\u0026#39;)) ] async def stop(self): \u0026#34;\u0026#34;\u0026#34;优雅关闭\u0026#34;\u0026#34;\u0026#34; self.running = False await asyncio.gather(*self.monitor_tasks, return_exceptions=True) await self.exchange_a.close() await self.exchange_b.close() 上面实现了一个 Monitor 类，只要监听一个新的价格，都会重新计算 spread 价差，评估是否触发报警或者进入到是否交易的验证中。\n如下代码，导入前面准备的 normal_pairs.csv 中的交易对，监控价差：\nasync def main(exchange_a, exchange_b, pairs): await exchange_a.load_markets() await exchange_b.load_markets() monitor = Monitor(exchange_a, exchange_b, pairs) monitor.start() try: while True: await asyncio.sleep(1) # 可在此处可添加其他逻辑： except KeyboardInterrupt: await monitor.stop() print(\u0026#34;监控已停止\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: try: pairs_df = pd.read_csv(\u0026#34;normal_pairs.csv\u0026#34;) pairs_dif = pairs_df[pairs_df[\u0026#39;quote\u0026#39;] == \u0026#34;USDT\u0026#34;] required_cols = [\u0026#39;base\u0026#39;, \u0026#39;quote\u0026#39;, \u0026#39;symbol_a\u0026#39;, \u0026#39;symbol_b\u0026#39;] if not all(col in pairs_df.columns for col in required_cols): raise ValueError(\u0026#34;CSV文件缺少必要列\u0026#34;) pairs = pairs_df[required_cols].to_dict(\u0026#39;records\u0026#39;) except Exception as e: print(f\u0026#34;配置加载失败: {str(e)}\u0026#34;) exit(1) # 交易所初始化 exchange_a = ccxtpro.binance(params) exchange_b = ccxtpro.okx(params) try: asyncio.run(main(exchange_a, exchange_b, pairs)) except KeyboardInterrupt: print(\u0026#34;程序已终止\u0026#34;) 输出：\n套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 2.14% 套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 1.99% 套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 1.99% 套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 1.99% 套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 2.02% 套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 2.11% 套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 2.11% 套利机会! (\u0026#39;GLM\u0026#39;, \u0026#39;USDT\u0026#39;) 价差: 1.93% 如何应用？ # 监控得到价差数据后，怎么用呢？\n没有成熟的自动化交易策略的话，可以先做个报警消息观察观察，或者基于它做个实时排序，通过页面实时监控这些品种的价差信息。\n用于自动化交易的话肯定有风险的，实盘要考虑问题比较多，价差和实际的交易是有区别的，真实交易肯定要看 order book 的买卖价，对于流动性不足的品种，最近成交价和实际能交易的价格可能会相差很大，滑点很大。\n价差监控最好是用 orderbook 的买卖价和深度。我这里用最新价是因为监听 ticker 的数据量会小很多，order book 实时数据频率非常高，监控太多品种的负载太高。\n真实交易，除了滑点，还有交易费用，对于保证金和合约交易还有借贷利息、资金费率等成本，都是要考虑的。如果不解决这些问题，得到的可能就是不断亏损的套利。\n在成熟的市场，我觉得普通人是很难有这种套利机会的，不过加密货币市场，小币的套利机会应该还是有的。还有就是，这个市场每隔个一两月就会有一次异常的暴跌，这或许是价差套利的最佳时机，当然没有控制好风险，也可能爆仓。\n再次声明，这是我的一次研究尝试，如要在真实场景下使用，请自行研究风险。\n最后，希望本文对你有用。\n","date":"2025-02-16","externalUrl":null,"permalink":"/posts/2025-02-16-arbitrage-between-cryptocurrency-exchanges-using-ccxt/","section":"文章","summary":"前面写了一篇关于如何用 Python 获取数字货币行情的博文，有评论希望我展开来说说跨交易所的套利机会。一直觉得随着参与这类策略的人多了，机会很少了，就没有深入研究过。这次借这个机会尝试一下。\n","title":"实时监控加密货币跨交易所的套利机会","type":"posts"},{"content":"前面写了一篇关于 python 获取加密货币行情 的文章，但如果要获取数据，还要亲自写代码，用起来不方便。\n今天，分享一个简短的脚本，从 binance 下载全量的行情数据，包括分钟级别的 OHLCV 行情。\n如下载 BTC/USDT 的 2021-01-01 到 2023-01-01 的全量分钟数据，指定如下命令即可。\npython data.py --symbol BTC/USDT --start 2021-01-01 --end 2023-01-01 --timeframe 1m --save-dir ./data 它会将下载的数据保存为 CSV 文件，适合自己平时分析研究的时候使用。\n下面把代码和过程分享一下。\n首先，需要安装以下 Python 库：\npip install ccxt pandas click python-dateutil pytz 其中：\nccxt 用于连接 Binance，获取行情数据； pandas 负责数据处理和保存成 CSV 文件； click 让代码可以用命令行的方式调用，参数传递更方便。 特别提醒：如果遇到网络问题，要自己想办法解决了。\n如下是完整的代码，可保存为 data.py 文件。\nimport os import datetime import pytz import pandas as pd import ccxt import click from dateutil.relativedelta import relativedelta from pathlib import Path exchange = ccxt.binance( { \u0026#34;enableRateLimit\u0026#34;: True, # 必须开启！防止被交易所封IP \u0026#34;timeout\u0026#34;: 15000 # 超时设为15秒 \u0026#34;proxies\u0026#34;: { \u0026#34;http\u0026#34;: os.getenv(\u0026#34;http_proxy\u0026#34;), \u0026#34;https\u0026#34;: os.getenv(\u0026#34;https_proxy\u0026#34;), } } ) def download(symbol: str, start=None, end=None, timeframe=\u0026#34;1d\u0026#34;, save_dir=\u0026#34;.\u0026#34;): if end is None: end = datetime.datetime.now(pytz.UTC) else: end = end.replace(tzinfo=pytz.UTC) if start is None: start = end - relativedelta(years=3) else: start = start.replace(tzinfo=pytz.UTC) max_limit = 1000 since = start.timestamp() end_time = int(end.timestamp() * 1e3) # Create save directory if it doesn\u0026#39;t exist Path(save_dir).mkdir(parents=True, exist_ok=True) absolute_path = os.path.join( save_dir, f\u0026#34;{symbol.replace(\u0026#39;/\u0026#39;, \u0026#39;-\u0026#39;)}_{timeframe}.csv\u0026#34; ) ohlcvs = [] while True: new_ohlcvs = exchange.fetch_ohlcv( symbol, since=int(since * 1e3), timeframe=timeframe, limit=max_limit, params={\u0026#34;endTime\u0026#34;: end_time } ) if len(new_ohlcvs) == 0: break ohlcvs += new_ohlcvs since = ohlcvs[-1][0]/1e3 + 1 print(f\u0026#34;下载进度：{datetime.datetime.fromtimestamp(ohlcvs[-1][0]/1e3)}\\r\u0026#34;, end=\u0026#34;\u0026#34;) print() data = pd.DataFrame( ohlcvs, columns=[\u0026#34;timestamp_ms\u0026#34;, \u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;, \u0026#34;volume\u0026#34;] ) data.drop_duplicates(inplace=True) data.set_index( pd.DatetimeIndex(pd.to_datetime(data[\u0026#34;timestamp_ms\u0026#34;], unit=\u0026#34;ms\u0026#34;, utc=True)), inplace=True, ) data.index.name = \u0026#34;datetime\u0026#34; del data[\u0026#34;timestamp_ms\u0026#34;] data.to_csv(absolute_path) print(f\u0026#34;Data saved to: {absolute_path}\u0026#34;) @click.command() @click.option(\u0026#34;--symbol\u0026#34;, required=True, help=\u0026#34;Trading symbol (e.g. BTC/USDT)\u0026#34;) @click.option(\u0026#34;--start\u0026#34;, type=click.DateTime(), help=\u0026#34;Start date (YYYY-MM-DD)\u0026#34;) @click.option(\u0026#34;--end\u0026#34;, type=click.DateTime(), help=\u0026#34;End date (YYYY-MM-DD)\u0026#34;) @click.option(\u0026#34;--timeframe\u0026#34;, default=\u0026#34;1d\u0026#34;, help=\u0026#34;Timeframe for OHLCV data (1m, 5m, 15m, 1h, 1d, etc.)\u0026#34;) @click.option(\u0026#34;--save-dir\u0026#34;, default=\u0026#34;.\u0026#34;, help=\u0026#34;Directory to save the CSV file\u0026#34;) def main(symbol, start, end, timeframe, save_dir): \u0026#34;\u0026#34;\u0026#34;Download OHLCV data from Binance\u0026#34;\u0026#34;\u0026#34; download(symbol=symbol, start=start, end=end, timeframe=timeframe, save_dir=save_dir) if __name__ == \u0026#34;__main__\u0026#34;: main() 注意：脚本统一用的 UTC 时区的时间。\n这个脚本通过 Binance 的 API 获取指定交易对的历史数据。默认下载最近 3 年的数据，当然你也可以通过命令行参数指定时间范围。\n时间周期可以选择 1 分钟到 1 天等多种类型，比如：\n1m（1 分钟） 5m（5 分钟） 1h（1 小时） 1d（1 天） 1w（1 周）等 下载的数据会保存成 CSV 文件，文件名形如 BTC-USDT_1d.csv，方便后续分析。\n假设你想下载 BTC/USDT 交易对在 2021 年到 2023 年期间的日线数据，可以在命令行中运行：\npython data.py --symbol BTC/USDT --start 2021-01-01 --end 2023-01-01 --timeframe 1d --save-dir ./data 脚本会自动下载数据，并保存到 ./data 目录下的 BTC-USDT_1d.csv 文件中。\n这个脚本算是一个小工具，虽然代码不多，但挺实用的，能省去很多重复工作。如果有需要，也可以扩展成自动化脚本，定时更新最新的数据。\n","date":"2025-02-12","externalUrl":null,"permalink":"/posts/2025-02-12-download-cryptocurrency-minute-candlestick/","section":"文章","summary":"前面写了一篇关于 python 获取加密货币行情 的文章，但如果要获取数据，还要亲自写代码，用起来不方便。\n今天，分享一个简短的脚本，从 binance 下载全量的行情数据，包括分钟级别的 OHLCV 行情。\n","title":"分享一个全量下载数字货币分钟行情的代码片段","type":"posts"},{"content":"在设计一个量化策略时，常会面临这样的疑问：我的策略好不好？怎么判断它是否真正有效？\n本文将尝试从这个角度触发，思考如何判断一个策略的好坏。\n概要引言 # 前面写了几篇博文，介绍如何用 backtesting.py 开发策略，当一个策略回测完成后会得到如下的信息。\nStart 2004-08-19 00:00:00 # 回测开始时间 End 2013-03-01 00:00:00 # 回测结束时间 Duration 3116 days 00:00:00 # 回测总时长 Exposure Time [%] 61.545624 # 持仓暴露时间百分比（处于市场中的时间比率） Equity Final [$] 99485.0574 # 回测结束时的账户净值 Equity Peak [$] 100607.2574 # 回测期间账户达到的最高净值 Return [%] 894.850574 # 总收益率（百分比） Buy \u0026amp; Hold Return [%] 703.458242 # 买入并持有策略的收益率（百分比），用于对比 Return (Ann.) [%] 30.934891 # 年化收益率（百分比） Volatility (Ann.) [%] 32.215003 # 年化波动率（百分比），衡量收益波动性 Sharpe Ratio 0.960263 # 夏普比率（风险调整收益率） Sortino Ratio 2.125336 # 索提诺比率（只考虑下行风险的风险调整收益率） Calmar Ratio 1.497421 # 卡玛比率（年化收益率与最大回撤的比值） Max. Drawdown [%] -20.658779 # 最大回撤（百分比），历史上账户净值的最大跌幅 Avg. Drawdown [%] -3.678412 # 平均回撤（百分比），所有回撤期间的平均亏损 Max. Drawdown Duration 584 days 00:00:00 # 最大回撤持续时间（最长亏损期） Avg. Drawdown Duration 38 days 00:00:00 # 平均回撤持续时间 # Trades 48 # 总交易次数 Win Rate [%] 64.583333 # 胜率（百分比），盈利交易的比率 Best Trade [%] 57.11931 # 最佳交易收益率（百分比） Worst Trade [%] -12.446769 # 最差交易收益率（百分比） Avg. Trade [%] 4.923007 # 平均每笔交易的收益率（百分比） Max. Trade Duration 121 days 00:00:00 # 最长单笔交易持续时间 Avg. Trade Duration 39 days 00:00:00 # 平均单笔交易持续时间 Profit Factor 4.729158 # 盈亏比（盈利总额与亏损总额的比值） Expectancy [%] 5.66852 # 期望收益率（百分比），长期平均每笔交易收益 SQN 2.643361 # 系统质量数（衡量交易系统稳定性的指标） 上面有很多指标，具体都是什么含义呢？\n评价一个策略要包含多个角度，不同的角度有对应指标衡量策略相应的表现。\n如果你比较在意收益，就得关注那些能直观反映盈利情况的指标，比如总收益率、年化收益率等。\n如果你更担心风险，那就得看看可能的最大亏损，比如最大回撤，了解在最糟糕情况下你的资金可能承受多少损失。\n一个策略的好坏往往不仅仅取决于收益或风险单一方面，还可以将两者同时考虑进来。\n除了收益与风险，其他维度，如交易频率，交易太频繁可能会增加成本和操作难度，而交易频率过低又可能错失良机。\n对于短线交易，胜率和盈亏比也是一个不可忽略的因素\n盈利能力 # 衡量策略盈利能力最直接的方式就是查看 总收益率。但仅仅看总收益可能并不全面，你还需要考虑到收益的稳定性。\n年化收益率：年化收益率能帮助你更好地评估长期的盈利能力，它不仅考虑了总收益，还考虑了时间因素。如果年化收益率过低，意味着策略的盈利潜力有限。\n上面策略的总收益为 894.85%。从数据来看，单纯的回报看起来非常好。但年化收益率（Return (Ann.) [%]）为30.93%，这表示你每年平均获得30.93%的收益，这样的年化收益在市场中算是相当不错的。\n风险评估 # 交易策略不仅要追求盈利，更重要的有效控制风险。我经常听到一句话，成功的交易一定能很好的管理风险的。最大回撤和波动率 就是两个量化策略风险的指标。\n最大回撤：最大回撤是指策略在某一周期内，账户从最高点到最低点的最大亏损幅度。\n波动率：波动率用于衡量收益的波动程度。如果波动率过大，意味着策略在盈利的同时可能也会伴随较大的亏损风险。\n在上面的回测报告中，策略的最大回撤为 -20.66%。这意味着，尽管策略有不错的回报，但在某些市场情况下，策略的资金也曾经历过接近21%的亏损。年化波动率为 32.22%，表明该策略在年度内的波动幅度较大，但考虑到它有着 30.93% 的年化收益率，也不算太差。\n风险与收益 # 单纯的盈利数据可能掩盖了潜在的风险，因此，评估策略时需要加入风险调整后的收益指标，如夏普比率和索提诺比率。\n夏普比率（Sharpe Ratio）：夏普比率用来衡量单位风险带来的超额收益，公式为： $$ \\text{Sharpe Ratio} = \\frac{\\text{Annualized Return} - \\text{Risk-Free Rate}}{\\text{Annualized Volatility}} $$在这个回测中，夏普比率为0.96，虽然不算很高，但也说明策略在风险控制上做得较为稳定。\n索提诺比率（Sortino Ratio）：与夏普比率类似，但索提诺比率只关注下行风险，这适合那些特别注重防止亏损的交易者。毕竟，对交易而言，下行风险才是风险。\n其公式为： $$ \\text{Sortino Ratio} = \\frac{\\text{Annualized Return} - \\text{Risk-Free Rate}}{\\text{Downside Deviation}} $$回测报告中，索提诺比率为2.13，表示策略在承担下行风险时，能获得相对较高的收益。\n胜率和盈亏比 # 如果你是做短线交易，胜率和盈亏比是非常重要的指标。\n胜率：胜率是你在所有交易中获胜的比率。\n盈亏比（Profit Factor）：是你每次盈利的金额与亏损的金额之比。\n在这个回测案例中，策略的胜率为64.58%，表明超过一半的交易是成功的，表现不错。然而，胜率的高低不能单独作为好坏的标准，还需要结合盈亏比来评估。盈亏比 为4.73，说明每赚 4.73 美元，亏损大约是 1 美元，表现非常良好。盈亏比高说明策略的风险回报比良好，即便胜率不算非常高，仍然可以盈利。\n交易频率 # 交易频率反映了你策略的活跃度，不同的市场环境可能需要不同的操作节奏。你可以根据策略的性质来判断交易频率是否合适。\n高频交易策略：如果你的策略是基于短期波动的快速决策，那么频繁的交易可能是必需的。但这种策略对市场的流动性要求较高，风险也较大。\n低频交易策略：比如趋势跟随或者长期持仓策略，它们通常更加稳定，但可能错过一些短期的机会。\n我测试的这个策略是趋势最总策略，3000 多天的 交易总次数 为 48 次，频率不高，说明这是一个相对低频的交易策略。\n综合评价指标 # 前面讲了评价策略的各个维度的指标，是否还记得之前的 backtesting.py 优化入门那篇文章，里面还有一个评价指标，SQN。\nSQN 是衡量交易策略稳健性的一个综合指标，其计算公式为：\n$$ SQN = (平均每笔交易收益 × √交易次数) / 每笔交易收益的标准差 $$一句话解释它的含义：SQN 通过结合平均收益（越高越好）、收益波动性（越低越好）以及交易次数（越多越好），来衡量策略的稳健性和质量，值越高代表策略表现越优。从指标的公式可以看出，这个指标即考虑收益还考虑风险，同时包括交易的次数。\n如上的回测报告中的 SQN 值为 2.643361，说明策略在收益、风险和交易频率这几个维度上已经取得了较好的平衡。\n回测与实盘 # 在补充几句关于回测与实盘的看法。\n回测能提供一个策略框架，但真正的考验是策略在实盘中的表现。可能回测结果好，但实际操作中却无法复刻那样的效果。\n回测的好处：通过历史数据的回测，你可以初步了解策略的优缺点、潜在的收益和风险。\n实盘的挑战：真实的市场中，你可能会面临滑点、手续费、情绪等额外的因素，这些都可能影响策略的表现。\n回测结果只是一个参考，但并不是说它不重要，如果连回测都无法做到好的表现，实盘表现出色的概率基本为无。\n总结 # 评价交易策略的表现并不复杂，只需要从盈利能力、风险管理、胜率和盈亏比等几个核心指标入手。在实际操作中，可能还要适时调整策略的交易频率、控制风险，。\n希望本文能让你更清晰地评价自己的交易策略，帮助自己做出更好的决策，避免过度看中收益，而忽略其他评价维度。\n","date":"2025-02-11","externalUrl":null,"permalink":"/posts/2025-02-11-evaluate-a-trading-strategy-performance/","section":"文章","summary":"在设计一个量化策略时，常会面临这样的疑问：我的策略好不好？怎么判断它是否真正有效？\n本文将尝试从这个角度触发，思考如何判断一个策略的好坏。\n概要引言 # 前面写了几篇博文，介绍如何用 backtesting.py 开发策略，当一个策略回测完成后会得到如下的信息。\n","title":"如何评价交易策略的表现？","type":"posts"},{"content":"如果你想参与加密货币市场，获取实时、准确的市场数据就是重要的第一步。\n不同于传统金融市场，加密货币市场上的交易所有两类，分别 DEX 和 CEX，即中心化交易所和去中心化交易所。两者的区别这里就不展开了，我们大部分人接触的一般都是中心化交易所。和传统金融市场一样，加密货币市场上的数据，我们也可找专业的数据提供商，但基本都是付费的。\n本文将介绍如何通过 Python 从中心化交易所拿到行情数据，包括历史和实时数据，包括直接从 websocket 监控数据，可用来实时监听不对交易对间的价差数据。\n概要说明 # 从去中心化交易所拿到交易所数据，可直接通过访问它的 HTTP 和 Websocket 接口，如欧易 OKX 的接口，查看它的官方文档：欧易 API 文档，其他每个交易所也都有这类文档，可查看其支持的 API 接口。\n不过市面上的中心化交易所很多，而且如果要获取账户级别的信息，还有经过认证流程。本文为了简化流程，覆盖更多的去中心化交易所，我会使用一个非常流行的 Python 库 ccxt 演示，从而把重点集中在数据的获取方法上。\n什么是 ccxt # ccxt，全称 CryptoCurrency eXchange Trading，这个开源库能让你用统一的代码访问全球上百家数字货币交易所。它支持多个编程语言，如 Javascript、Python 和 PHP。\nccxt，除了可用来获取数据，还可以支持直接下单、订单管理、访问仓位、账户等，用来实现一个自动化交易人。\n接下来开始正文吧！\n本文将从最简单的价格获取到复杂的订单簿处理，带你一步步深入。\n安装 ccxt # 打开终端，输入如下命令，安装 ccxt 库：\npip install ccxt 初始化交易所 # 以币安为例，先创建一个交易所对象：\nimport ccxt exchange = ccxt.binance({ \u0026#39;enableRateLimit\u0026#39;: True, # 必须开启！防止被交易所封IP \u0026#39;timeout\u0026#39;: 15000 # 超时设为15秒 }) 交易所的API都有严格的调用频率限制，enableRateLimit 会启用频率限制能力，防止触发交易所的 API 调用频率。\n如果遇到网络问题，也可通过设置 proxies 切换网络环境：\nexchange = ccxt.okx({ \u0026#34;proxies\u0026#34;: { \u0026#34;http\u0026#34;: \u0026#34;http://127.0.0.1:7890\u0026#34;, \u0026#34;https\u0026#34;: \u0026#34;http://127.0.0.1:7890\u0026#34;, } }) 获取交易所上的所有品种：\nmarkets = exchange.load_markets() print(list(markets.keys)[:10]) 输出如下：\n[\u0026#39;ETH/BTC\u0026#39;, \u0026#39;LTC/BTC\u0026#39;, \u0026#39;BNB/BTC\u0026#39;, \u0026#39;NEO/BTC\u0026#39;, \u0026#39;QTUM/ETH\u0026#39;, \u0026#39;EOS/ETH\u0026#39;, \u0026#39;SNT/ETH\u0026#39;, \u0026#39;BNT/ETH\u0026#39;, \u0026#39;BCC/BTC\u0026#39;, \u0026#39;GAS/BTC\u0026#39;] 在开始其他操作前，这一步是必要的，否则你可能会遇到品种不存在的问题。\n实时价格 # 首先，示例如何通过 ccxt 获取某个品种当前的价格，主要依赖于 fetch_ticker 函数接口。\n示例代码，读取 BTC/USDT 交易对的最新价。\nticker = exchange.fetch_ticker(\u0026#39;BTC/USDT\u0026#39;) print(f\u0026#34;\u0026#34;\u0026#34; 实时价格： - 最新价：{ticker[\u0026#39;last\u0026#39;]} USDT - 24小时最高：{ticker[\u0026#39;high\u0026#39;]} - 24小时成交量：{ticker[\u0026#39;quoteVolume\u0026#39;]} USDT \u0026#34;\u0026#34;\u0026#34;) 输出：\n实时价格： - 最新价：97004.87 USDT - 24小时最高：97640.02 - 24小时成交量：1743212485.2952676 USDT ticker 的结构体可查看官方文档 Ticker Structure。\n{{ \u0026#39;symbol\u0026#39;: string 类型，表示市场的交易对（例如：\u0026#39;BTC/USD\u0026#39;, \u0026#39;ETH/BTC\u0026#39; 等）， \u0026#39;info\u0026#39;: 原始未修改的交易所 API 回复信息（未经解析的原始数据）， \u0026#39;timestamp\u0026#39;: integer 类型，64 位 Unix 时间戳，单位为毫秒，自 1970 年 1 月 1 日起， \u0026#39;datetime\u0026#39;: ISO8601 格式的日期时间字符串（带毫秒）， \u0026#39;high\u0026#39;: float 类型，市场的最高价格， \u0026#39;low\u0026#39;: float 类型，市场的最低价格， \u0026#39;bid\u0026#39;: float 类型，当前最佳买入价， \u0026#39;bidVolume\u0026#39;: float 类型，当前最佳买入量（可能缺失或未定义）， \u0026#39;ask\u0026#39;: float 类型，当前最佳卖出价， \u0026#39;askVolume\u0026#39;: float 类型，当前最佳卖出量（可能缺失或未定义）， \u0026#39;vwap\u0026#39;: float 类型，加权平均价格， \u0026#39;open\u0026#39;: float 类型，开盘价， \u0026#39;close\u0026#39;: float 类型，最后交易价格（当前周期的收盘价）， \u0026#39;last\u0026#39;: float 类型，等同于 `close`，为方便重复表示， \u0026#39;previousClose\u0026#39;: float 类型，上一个周期的收盘价， \u0026#39;change\u0026#39;: float 类型，绝对变化，`last - open`， \u0026#39;percentage\u0026#39;: float 类型，相对变化，`(change/open) * 100`， \u0026#39;average\u0026#39;: float 类型，平均价格，`(last + open) / 2`， \u0026#39;baseVolume\u0026#39;: float 类型，过去 24 小时内交易的基础货币的交易量， \u0026#39;quoteVolume\u0026#39;: float 类型，过去 24 小时内交易的报价货币的交易量， } 简言之，fetch_ticker 返回的是某个交易对的当前最新的市场数据。\n历史行情 # 除了最新行情，历史行情对于交易也非常有帮助，如我们用它来计算指标，识别模式，产生交易信号。\n如下是获取 BTC/USDT 的日线数据的代码：\n# 获取最近3天的日线数据 ohlcvs = exchange.fetch_ohlcv( symbol=\u0026#39;BTC/USDT\u0026#39;, timeframe=\u0026#39;1d\u0026#39;, # 可选：1m, 5m, 1h, 1d 等 limit=5 # 获取条数 ) print(ohlcvs) 参数设置获取 BTC/USDT 的历史行情，时间周期为 1d，数量为 5。\n输出：\n[[1739138760000, 94897.63, 95052.0, 94884.44, 95047.09, 17.96677], [1739138820000, 95047.1, 95128.75, 95047.1, 95118.36, 12.27411], [1739138880000, 95118.36, 95135.99, 95051.65, 95122.51, 10.40116], [1739138940000, 95122.52, 95178.69, 95104.42, 95178.57, 7.81859], [1739139000000, 95178.57, 95246.13, 95160.0, 95246.13, 48.00226]] 列表中的每个元素依然是一个列表，从左到右分别是时间戳、开盘价、最高价、最低价、收盘价和交易量。\n可以将其转为更加易读的 pandas 格式，如下所示；\ndata = pd.DataFrame(ohlcvs, columns=[\u0026#34;timestamp_ms\u0026#34;, \u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;, \u0026#34;volume\u0026#34;]) print(data) 输出：\ntimestamp_ms open high low close volume 0 1739139000000 95178.57 95246.13 95160.00 95246.13 48.00226 1 1739139060000 95246.12 95436.12 95230.02 95429.63 24.59766 2 1739139120000 95429.64 95429.64 95282.44 95371.00 44.81550 3 1739139180000 95371.00 95443.28 95371.00 95433.12 12.77712 4 1739139240000 95433.12 95527.79 95433.12 95436.70 15.98278 如果想设计一个高频策略，可能还要拿到分钟级别数据，很多交易所都支持拿到分钟级别数据，甚至是全量的分钟数据。\n假设，获取 BTC/USDT 的 1 分钟数据，只需修改 fetch_ohlcv 的 timeframe 参数即可。\n示例代码:\n# 获取最近20根15分钟K线 ohlcvs = exchange.fetch_ohlcv(\u0026#39;BTC/USDT\u0026#39;, \u0026#39;1m\u0026#39;, limit=20) 订单簿（市场深度） # 除了行情数据，如果你拿到订单薄数据，ccxt 也可以做到，只要调用 fetch_order_book 即可。\n一个简单示例：\norder_book = exchange.fetch_order_book(\u0026#39;BTC/USDT\u0026#39;, limit=100) best_bid = order_book[\u0026#39;bids\u0026#39;][0][0] # 买一价 best_ask = order_book[\u0026#39;asks\u0026#39;][0][0] # 卖一价 print(f\u0026#34;买一价：{best_bid}, 卖一价：{best_ask}, 价差：{best_ask - best_bid}\u0026#34;) 输出：\n买一价：97141.35, 卖一价：97141.36, 价差：0.00999999999476131 其中，fetch_order_book 可用于指定获取深度，像 binance 的接口默认是 100，而最多可以获取到 5000 的订单薄深度。\n实时监听 # 前面介绍完的几种数据接口，都是我们主动发起请求，对于一些实时性要求非常高的场景，轮询延迟大，丢掉一些交易机会，且可能触发交易所的频率限制。ccxt 还提供了一个 pro 版本 ccxt.pro，可直接通过 WebSocket 实时监听。\n前面的接口，我们简单修改下，就可以实现实时监听的效果。\nimport asyncio import ccxt.pro as ccxtpro exchange = ccxtpro.binance({ \u0026#39;enableRateLimit\u0026#39;: True, # 必须开启！防止被交易所封IP \u0026#39;timeout\u0026#39;: 15000, # 超时设为15秒 }) async def watch_order_book(): while True: order_book = await exchange.watch_order_book(\u0026#39;BTC/USDT\u0026#39;) best_ask = order_book[\u0026#34;asks\u0026#34;][0][0] best_bid = order_book[\u0026#34;bids\u0026#34;][0][0] print(f\u0026#34;order_book, 价差:{best_ask-best_bid}\u0026#34;) async def watch_ticker(): while True: ticker = await exchange.watch_ticker(\u0026#39;BTC/USDT\u0026#39;) print(f\u0026#34;ticker, 最新价: {ticker[\u0026#39;last\u0026#39;]}\u0026#34;) async def main(): await asyncio.gather(watch_order_book(), watch_ticker()) if __name__ == \u0026#34;__main__\u0026#34;: asyncio.run(main()) 输出：\norder_book, 价差:0.00999999999476131 order_book, 价差:0.00999999999476131 order_book, 价差:0.00999999999476131 order_book, 价差:0.00999999999476131 order_book, 价差:0.00999999999476131 order_book, 价差:0.00999999999476131 order_book, 价差:0.00999999999476131 order_book, 价差:0.00999999999476131 ticker, 最新价: 97228.01 ... 现在就能实时监控，不断输出新的数据。\n总结 # 现在，就已经掌握了如何利用 ccxt 获取市场行情，接下来可以：\n监控价差：实时计算不同交易对或交易所间的套利机会； 对接交易：用ccxt的 create_order 方法实现自动交易； 最后，希望本文对你有所帮助，感谢阅读。\n","date":"2025-02-10","externalUrl":null,"permalink":"/posts/2025-02-10-crypto-currency-market-data-using-ccxt/","section":"文章","summary":"如果你想参与加密货币市场，获取实时、准确的市场数据就是重要的第一步。\n不同于传统金融市场，加密货币市场上的交易所有两类，分别 DEX 和 CEX，即中心化交易所和去中心化交易所。两者的区别这里就不展开了，我们大部分人接触的一般都是中心化交易所。和传统金融市场一样，加密货币市场上的数据，我们也可找专业的数据提供商，但基本都是付费的。\n","title":"如何通过 Python 获取加密货币的历史实时行情","type":"posts"},{"content":"本文介绍如何使用 Python 开发一个 gmgn.ai 的抢币机器人，它可以用来从链上抢币，如 Trump 等 meme 币。\n风险提示： GMGN.AI 的币波动极大，有大量的空气币，务必谨慎操作，切勿在不了解的情况下投入过多资金。本文是我学习过程中的记录，请仔细甄别，防止资金损失。\n完整的实例代码请查看：sniper_new.py。\n什么是 GMGN.AI? # GMGN.AI 是一个代币追踪和分析平台，提供了实时市场信号和自动交易机器人，我们可以监听它提供的实时信号，跟单聪明钱包，快速抓住赚钱机会。\n通过 GMGN.AI 的分析能力筛选潜力代币，然后在它上就可以直接交易。\n除此以外，它还提供了自动化交易机器人。GMGN.AI 的自动化交易机器人都是依托于 telegram 实现的，它提供了很多机器人，如新币信号、聪明钱包等，都可以通过监听 telegram 消息实现，然后在基于它提供的交易机器人直接下单交易。\nGMGN.AI 内置机器人功能是固定的，如果我想自定义交易规则，如按持有人数、流动市值等过滤掉一些不满足条件的币，就需要自己写程序实现了。\n如何实现一个抢币机器人 # 在 GMGN.AI 实现自定义交易机器人高度依赖 Telegram，要通过 telegram 的 app 将我的交易机器人与 GMGN.AI 的 telegram 机器人连接。我将使用 Python 实现这个机器人。\n实现的大致流程如下：\n创建 Telegram App：在 Telegram 上创建一个应用，获取 api_id 和 api_hash。 监听 GMGN.AI 信号：GMGN.AI 会实时推送不同类型信号给 Telegram，机器人要监听刚兴趣的信号进行抢单。 按条件过滤信号：监听到信号消息，将消息文本转为结构化的数据，判断是否下达交易。 通过 Sniper Bot 执行交易：一旦确认某个币符合条件，发送指定给 Sniper Bot 下单。 安装依赖包 # 提前安装好依赖库：\npip install telethon instructor pydantic openai Telethon 库是用于和 telegram 交互的，而 instructor 是要与大模型配合将 Telegram 文本消息结构化的；\n创建 Telegram App # 访问 Telegram Developer，使用 Telegram 账户登录。接下来会看到如下的页面。\n填写这个表单，创建应用，就能获得认证所要的 api_id 和 api_hash，实现程序和 Telegram 交互的身份验证。\n验证登录 # 正式开始机器人的逻辑前，先用 Telethon 来登录 Telegram，验证下 api_id 和 api_hash 是否正确。\n实现代码如下：\nfrom telethon import TelegramClient, events api_id = \u0026#39;YOUR_API_ID\u0026#39; api_hash = \u0026#39;YOUR_API_HASH\u0026#39; # 创建 Telegram 客户端 client = TelegramClient(\u0026#39;session_name\u0026#39;, api_id, api_hash) async def main(): # 登录 Telegram await client.start() print(\u0026#34;登录成功！\u0026#34;) # 获取当前用户信息 me = await client.get_me() print(f\u0026#34;用户名：{me.username}，ID：{me.id}\u0026#34;) await client.run_until_disconnected() client.loop.run_until_complete(main()) 如果认证成功，会打印 \u0026ldquo;登录成功\u0026rdquo; 和我的用户信息。\n登录成功！ 用户名：xxx，ID：xxx 信号来源 # GMGN.AI 提供一些信号群，如 t.me/gmgnsignals 和 t.me/gmgnsignalsol， 会实时推送不同主题的信号消息。\n如下是 GMGN.AI 的官方文档上给出的信号列表：\nCTO信号 (社区接管） Update DEX Screener Social Infos (更新社交三件套) PUMP Update DEX Screener Social Infos (PUMP币更新社交三件套) PUMP FDV Surge (PUMP内盘代币快速飙升) Solana FDV Surge (Solana代币快速飙升) Smart Money FOMO (聪明钱FOMO) KOL FOMO DEV Burnt Alert (作者烧币) ATH Price (历史新高) Heavy Bought (大单买入) Sniper New (狙新币) 我将以监听 \u0026ldquo;Sniper New（狙击新币）\u0026rdquo; 消息为例，演示如何狙击满足我要求的新币。\n监听 \u0026ldquo;Sniper New\u0026rdquo; 消息 # 首先，\u0026ldquo;Sniper New\u0026rdquo; 消息位于 https://t.me/gmgnsignalsol 群组下。我将用 telethon 的消息监听能力监听 @gmgnsignalsol 下的 \u0026ldquo;Sniper New\u0026rdquo; 消息。\n先查看下消息的格式：\n🎯Featured New Pair🎯 $PEPSI(Pepsi On Solana) 4ZsJoreG6w6xpgHPtaBBA6aSr8EzHVoDdezAkxeFVJvG 📈 5m | 1h | 6h: \u0026gt;99999% | \u0026gt;99999% | \u0026gt;99999% 🎲 5m TXs/Vol: 54/$93.9K 💡 MCP: $857.1M 💧 Liq: 453.11 SOL ($186.8K 🔥100%) 👥 Holder: 136 🕒 Open: 16s ago ✅ NoMint / ✅Blacklist / ✅Burnt ✅TOP 10: 13.52% ⏳ DEV: Add Liquidity 👨‍🍳 DEV Burnt烧币: 0(🔥Rate: %) Backup BOT: US | 01 | 02 | 03 | 04 🌏 Website | ✈️ Telegram 🌈 NEW: Wallet Copy Trading is Live! Click NOW 这个消息的特点是开头包含 \u0026ldquo;Featured New Pair\u0026rdquo; 文本。\n如下是实现代码：\n@client.on(events.NewMessage( chats=\u0026#34;@gmgnsignalsol\u0026#34;, pattern=\u0026#34;.*Featured New Pair.*\u0026#34; )) def on_new_sniper(event): print(event.message.text) 如上的代码，events.NewMessage 的两个参数 chats 和 pattern 是用来限定监听什么样的信号。\n到此，我的这个机器人就能实时监听新币消息了。\n文本消息结构化 # 现在这个消息还是是文本内容，并不便于接下来的分析，程序中起码要能拿到如代币的合约地址、持有人数、市值的等信息。最起码的，要能拿到合约地址吧。\n如何将文本结构化呢？\n一种常用方式是通过正则解析，但这要求文本格式固定，稍微有所改变就要重新解析。\n还有一种方式，通过 LLM 大语言模型实现文本结构化。deepseek 的出现让 LLM 的调用成本非常的低。如果在意成本，跑一个本地模型也能满足需求。\n首先要让大模型知道要提取什么数据，通过 pydantic 将目标定义出来。\n代码如下所示：\nfrom pydantic import BaseModel, Field from datetime import datetime class NewPair(BaseModel): contract_address: str = Field(..., description=\u0026#34;The contract address, used to uniquely identify the contract\u0026#34;) market_cap: float = Field(..., description=\u0026#34;The market capitalization of the contract, usually in USD\u0026#34;) liquidity_sol: float = Field(..., description=\u0026#34;Liquidity in SOL\u0026#34;) liquidity: float = Field(..., description=\u0026#34;Liquidity in USD\u0026#34;) holders_count: int = Field(..., description=\u0026#34;The number of holders of the contract\u0026#34;) open_seconds: int = Field(..., description=\u0026#34;The number of seconds since the contract was opened\u0026#34;) top10_ratio: float = Field(..., description=\u0026#34;The ratio of the top 10 holders, expressed as a decimal\u0026#34;) no_mint: bool = Field(..., description=\u0026#34;Whether the contract is NoMint, meaning no new tokens can be minted\u0026#34;) blacklist: bool = Field(..., description=\u0026#34;Whether the contract is blacklisted\u0026#34;) burnt: bool = Field(..., description=\u0026#34;Whether the tokens in the contract are burnt\u0026#34;) 接下来，用 LLM 按定义的模型从文本抽取数据即可。我用到一个名为 instructor 简化这个过程。\n实现代码如下：\nimport instructor from openai import AsyncOpenAI llm = instructor.from_openai(AsyncOpenAI( base_url=\u0026#34;https://api.deepseek.com\u0026#34;, api_key=os.getenv(\u0026#34;DEEPSEEK_API_KEY\u0026#34;), )) @client.on(events.NewMessage( chats=\u0026#34;@gmgnsignalsol\u0026#34;, pattern=\u0026#34;.*Featured New Pair.*\u0026#34;) ) async def on_sniper_new(event): response = await llm.chat.completions.create( model=\u0026#34;deepseek-chat\u0026#34;, response_model=NewPair, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: event.message.text}], ) print(response) 输出样例如下：\ncontract_address=\u0026#39;EX82HnkDihYGP8owXSDZfyMQXzovvYiWddNAxuxfpump\u0026#39; market_cap=87400.0 liquidity_sol=81.94 liquidity=34400.0 holders_count=523 open_seconds=120 top10_ratio=0.2207 no_mint=True blacklist=True burnt=True 现在，要按选币条件过滤新币，如 top10 的占比要小于 10%，只有人数要大于 500 人，流动市值要大于 100k USD。\nasync def on_sniper_new(event): r = await llm.chat.completions.create( model=\u0026#34;deepseek-chat\u0026#34;, response_model=NewPair, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: event.message.text}], ) if ( r.holders_count \u0026gt; 500 and r.liquidity \u0026gt; 10e5 and r.top10_ratio \u0026lt; 0.1 and r.blacklist and r.no_mint and r.burnt ): await create_buy_order(r.contract_address, 0.01) # 下单 0.01 SOL 到此，我已经按规则选择出来了我想要的币。现在还差最后一步，实现下单交函数 create_buy_order。\n通过 Sniper Bot 执行交易 # GMGN.AI 提供了在 Telegram 通过发送命令执行交易的机器人，如实现自动化交易。 SOL 连上的抢币机器人，请查看官方文档：TG Solana 狙击机器人。\n快速学习几个简单的使用机器人的命令案吧。\n立刻买入某合约 0.1 SOL： /buy EX82HnkDihYGP8owXSDZfyMQXzovvYiWddNAxuxfpump 0.1 立刻卖出持有仓位的 50%： sell EX82HnkDihYGP8owXSDZfyMQXzovvYiWddNAxuxfpump 50 还提供了很多其他的命令，如仓位、钱包余额、当前订单等等。\n到此，create_buy_order 实现起来就很简单了，如下所示：\nasync def create_buy_order(contract_address, amount): buy_command = f\u0026#34;/buy {contract_address} {amount}\u0026#34; await client.send_message(\u0026#34;@GMGN_sol03_bot\u0026#34;, buy_command) print(f\u0026#39;已执行交易: {buy_command}\u0026#39;) 到此，整个流程就打通了，接下来的重点就是找到能赚钱的策略了。\n总结 # 本文介绍了如何利用 Python 的 Telethon 库结合 GMGN.AI 提供的信号和 Sniper Bot 执行自动化抢币交易。\n再次说明：我开发这个机器人仅仅为学习目的，去打通了抢币的自动化流程。链上交易风险很大，要想真正赚钱，还要自行下功夫研究。\n","date":"2025-02-04","externalUrl":null,"permalink":"/posts/2025-02-04-create-a-trading-bot-on-gmgn-ai-using-python/","section":"文章","summary":"本文介绍如何使用 Python 开发一个 gmgn.ai 的抢币机器人，它可以用来从链上抢币，如 Trump 等 meme 币。\n风险提示： GMGN.AI 的币波动极大，有大量的空气币，务必谨慎操作，切勿在不了解的情况下投入过多资金。本文是我学习过程中的记录，请仔细甄别，防止资金损失。\n","title":"基于 Python 开发 GMGN.AI 抢币机器人","type":"posts"},{"content":"本文将介绍如何通过 Python、Streamlit 和 Tushare，搭建一个简单易用的股票筛选器，它不仅可以筛选股票，还能查看某个股票的详细数据生成动态 K 线图，让你对股票市场有更全面的了解。\n概述 # 这个股票筛选器提供将提供几个筛选能力，包括市值范围、行业、地域条件。还支持股票的 K 线图，帮助用户直观分析价格走势。\n效果如下：\n这个小工具的实现离不开一个关键的 Python 开源库 - Streamlit。它可以快速构建交互式 Web 应用，只需几行代码，即可分享你的数据。对 streamlit 有兴趣可以深入了解下，这里不展开介绍。\n如果不想看下面的详细步骤可直接从 github gist 拿到代码，完成第一部分的准备即可运行。\n想进一步扩展的话，还可以添加更多筛选指标，此外还可为单只股票增加技术指标（如均线、RSI）分析、多股票价格走势对比、实时数据更新、模拟投资组合功能甚至是按筛选分析结果生成股票分析报告等功能。\n接下来，讲解代码的每一步实现过程。\n准备开始 # 安装要用到 python 库：\npip install tushare ploty streamlit 并提前导入依赖包：\nimport streamlit as st import tushare as ts import pandas as pd import plotly.graph_objects as go 通过 pro = ts.pro\\_api() 配置了 Tushare 接口，为数据获取做好准备。\nts.set_token(\u0026#34;Your API Token\u0026#34;) pro = ts.pro_api() 注：tushare pro 依赖于 API Key，如果没有的话，到 tushare pro 平台注册获取。\n股票数据 # 为了完成股票数据的筛选，要用到 tushare 的两个，分别是 stock_basic 股票基础数据和 stock_daily 每日最新指标，这里有要用到的市值数据。\n基础信息 # 首先，获取所有上市股票的基本信息，包括股票代码、名称、行业、地域和上市日期。如果某些行业或地域数据缺失，用 \u0026ldquo;Unknown\u0026rdquo; 填补空值。\n@st.cache_data def get_stock_list(): df = pro.stock_basic( exchange=\u0026#34;\u0026#34;, list_status=\u0026#34;L\u0026#34;, fields=\u0026#34;ts_code,symbol,name,area,industry,list_date\u0026#34;, ) df[\u0026#34;area\u0026#34;] = df[\u0026#34;area\u0026#34;].fillna(\u0026#34;Unknown\u0026#34;) df[\u0026#34;industry\u0026#34;] = df[\u0026#34;industry\u0026#34;].fillna(\u0026#34;Unknown\u0026#34;) 为了提高性能和减少重复计算，这里用了 @st.cache_data 装饰器缓存数据结果，股票的基础数据是不怎么变化的，这样可避免每次交互重复加载数据。\n市值数据 # 为了筛选高市值股票，调用每日基础数据接口，获取市值数据，并将其与基本信息合并。\nmarket_cap_data = pro.daily_basic() market_cap_data[\u0026#34;total_mv\u0026#34;] = market_cap_data[\u0026#34;total_mv\u0026#34;] / 10000 # 转换为\u0026#34;亿\u0026#34; df = pd.merge(df, market_cap_data, on=\u0026#34;ts_code\u0026#34;, how=\u0026#34;left\u0026#34;) df = df.rename(columns={\u0026#34;total_mv\u0026#34;: \u0026#34;market_cap\u0026#34;}) 到这里，我们生成了一份包含公司基本信息和市值的完整股票列表，为后续筛选做好准备。\n界面和筛选条件 # 接下来，创建用户界面和筛选项。\nst.title(\u0026#34;Stock Screener\u0026#34;) st.sidebar.header(\u0026#34;Filter Options\u0026#34;) 侧边栏设置以下筛选条件：\n市值范围：设置筛选股票的最小和最大市值。 行业和地域：按行业或地域选择股票。 上市日期范围：筛选符合时间条件的公司。 筛选实现 # 以下代码实现了这些过滤功能：\n# 股票的基本信息 stocks = get_stock_list() # 筛选市值范围 min_market_cap = st.sidebar.number_input(\u0026#34;Minimum Market Cap (亿)\u0026#34;, min_value=0, value=100) max_market_cap = st.sidebar.number_input(\u0026#34;Maximum Market Cap (亿)\u0026#34;, min_value=0, value=1000) # 筛选行业和地域 industry_list = stocks[\u0026#34;industry\u0026#34;].unique().tolist() selected_industry = st.sidebar.multiselect(\u0026#34;Select Industry\u0026#34;, industry_list) area_list = stocks[\u0026#34;area\u0026#34;].unique().tolist() selected_area = st.sidebar.multiselect(\u0026#34;Select Area\u0026#34;, area_list) 根据这些条件，我们过滤股票数据：\nfiltered_stocks = stocks.copy() filtered_stocks = filtered_stocks[ (filtered_stocks[\u0026#34;market_cap\u0026#34;] \u0026gt;= min_market_cap) \u0026amp; (filtered_stocks[\u0026#34;market_cap\u0026#34;] \u0026lt;= max_market_cap) ] if selected_industry: filtered_stocks = filtered_stocks[filtered_stocks[\u0026#34;industry\u0026#34;].isin(selected_industry)] if selected_area: filtered_stocks = filtered_stocks[filtered_stocks[\u0026#34;area\u0026#34;].isin(selected_area)] 这样可以看到符合条件的股票列表。\n结果显示 # 我们继续通过 streamlit 的表格形式展示筛选后的股票列表，按照市值大小排序。\ndisplay_df = filtered_stocks.copy() display_df[\u0026#34;market_cap\u0026#34;] = display_df[\u0026#34;market_cap\u0026#34;].round(2) display_df = display_df.sort_values(by=\u0026#34;market_cap\u0026#34;, ascending=False) st.dataframe(display_df) 这样就可以清晰地看到结果，并快速找到目标股票。\n单只股票详情 # 有了筛选的股票列表后，还可以继续扩展，查看单只股票的情况，如蜡烛图、价格表格。\n日线数据 # 先通过 Tushare 接口获取用户选择的股票的日线数据，并根据日期范围过滤。\n@st.cache_data def get_daily_data(ts_code): return pro.daily(ts_code=ts_code) daily_data = get_daily_data(stock_code) daily_data[\u0026#34;trade_date\u0026#34;] = pd.to_datetime(daily_data[\u0026#34;trade_date\u0026#34;]) daily_data = daily_data.sort_values(\u0026#34;trade_date\u0026#34;, ascending=True) 筛选界面 # 配置筛选条件，包括股票选择框和日期范围。\n# Show details for selected stock selected_stock = st.selectbox(\u0026#34;Select a stock to view details\u0026#34;, filtered_stocks[\u0026#34;name\u0026#34;]) today = pd.Timestamp.today() col1, col2 = st.columns(2) with col1: start_date = st.date_input( \u0026#34;Start date\u0026#34;, value=today - pd.Timedelta(days=365), max_value=today ) with col2: end_date = st.date_input( \u0026#34;End date\u0026#34;, value=today, min_value=start_date, max_value=today ) 筛选逻辑 # 如果设置了 selected_stock 进入到筛选逻辑中。\nif selected_stock: stock_code = filtered_stocks[filtered_stocks[\u0026#34;name\u0026#34;] == selected_stock][ \u0026#34;ts_code\u0026#34; ].values[0] daily_data = get_daily_data(stock_code) daily_data[\u0026#34;trade_date\u0026#34;] = pd.to_datetime(daily_data[\u0026#34;trade_date\u0026#34;]) daily_data = daily_data.sort_values(\u0026#34;trade_date\u0026#34;, ascending=True) # 按时间过滤 daily_data = daily_data[ (daily_data[\u0026#34;trade_date\u0026#34;] \u0026gt;= pd.Timestamp(start_date)) \u0026amp; (daily_data[\u0026#34;trade_date\u0026#34;] \u0026lt;= pd.Timestamp(end_date)) ] # 检查缺失数据 if daily_data[[\u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;]].isnull().values.any(): st.warning(\u0026#34;Some price data is missing - filling with previous values\u0026#34;) daily_data[[\u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;]] = daily_data[ [\u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;] ].ffill() 绘制 K 线图 # 使用 Plotly 绘制动态 K 线图，让用户更直观地观察股票的价格变化。\nfig = go.Figure( data=[ go.Candlestick( x=daily_data[\u0026#34;trade_date\u0026#34;], open=daily_data[\u0026#34;open\u0026#34;], high=daily_data[\u0026#34;high\u0026#34;], low=daily_data[\u0026#34;low\u0026#34;], close=daily_data[\u0026#34;close\u0026#34;], increasing_line_color=\u0026#34;green\u0026#34;, decreasing_line_color=\u0026#34;red\u0026#34;, ) ] ) fig.update_layout(title=f\u0026#34;{selected_stock} Candlestick Chart\u0026#34;, xaxis_title=\u0026#34;Date\u0026#34;, yaxis_title=\u0026#34;Price\u0026#34;) st.plotly_chart(fig, use_container_width=True) 总结与扩展 # 通过以上步骤简单构建了一个股票筛选分析工具，它支持按市值、行业、地域和上市日期筛选股票，可视化 K 线图。\n希望本文对你有用，开始构建属于你的股票分析工具吧！\n","date":"2025-01-13","externalUrl":null,"permalink":"/posts/2025-01-13-create-a-stock-screener-using-streamlit/","section":"文章","summary":"本文将介绍如何通过 Python、Streamlit 和 Tushare，搭建一个简单易用的股票筛选器，它不仅可以筛选股票，还能查看某个股票的详细数据生成动态 K 线图，让你对股票市场有更全面的了解。\n","title":"使用 Streamlit 打造一个股票筛选分析工具","type":"posts"},{"content":"上篇文章介绍了如何使用 Backetsting.py 快速上手。今天继续介绍另一个策略回测时非常重要的点，参数优化。参数优化是提升策略表现的重要步骤，而 Backtesting.py 内置了参数优化功能，使用起来还是很方便的。\n本文将用上篇的案例 SMACrossStrategy 策略演示 Backtesting.py 参数优化。\n优化示例 # Backtesting.py 默认优化方式是将是将所有的参数组合穷举遍历一遍，这种方式也被称为-网格搜索。\n以下是优化 SMACross 策略的示例代码：\nbt = Backtest(GOOG, SMACrossStrategy, cash=10000, commission=0.002) stats = bt.optimize( fast_ma_window=range(5, 30, 5), slow_ma_window=range(10, 60, 10), ) print(stats) bt.plot() 通过 range 指定了 fast_ma_window 和 slow_ma_window 的参数范围，运行的结果就是表现最好的参数的结果。\n通过 stats._strategy 拿到策略实例，然后打印策略实例参数。\nprint(stats._strategy) print(\u0026#34;fast_ma_window\u0026#34;, stats._strategy.fast_ma_window) print(\u0026#34;slow_ma_window\u0026#34;, stats._strategy.slow_ma_window) 输出：\nSMACrossStrategy(fast_ma_window=5,slow_ma_window=20) fast_ma_window 5 slow_ma_window 20 网格搜索适用于参数空间较小的情况，超参数的场景还要考虑其他优化方法。先暂时只介绍这个默认的优化方法。\n参数约束 # 上面的优化代码有个明显的不合理，可能出现 fast_ma_window 大于 slow_ma_window 的情况，如 fast_ma_window=20，但 slow_ma_window=10 的组合，这不仅会增加优化耗时，也不符合策略的逻辑，。backtestingpy 的优化器提供了参数约束能力，我们可以限制 fast_ma_window 必须小于 slow_ma_window。\n示例代码：\nstats = bt.optimize( fast_ma_window=range(5, 30, 5), slow_ma_window=range(10, 60, 10), constraint=lambda p: p.fast_ma_window \u0026lt; p.slow_ma_window, ) 通过 constraint 指定参数约束函数，限制 fast_ma_window 必须小于 slow_ma_window。这能极大提升回测优化速度。\n优化标准 # 优化器默认只返回了一个表现最好的那个参数组合，但这个最好是如何评价的呢？\nBacktesting.py 优化器的默认优化标准是 SQN，它的计算公式如下：\nSQN = (平均每笔交易收益 × √交易次数) / 每笔交易收益的标准差 一句话表述它的含义就是，SQN 通过平均收益（高更好）、收益波动性（低更好）和交易次数（多更好）衡量策略稳健性，值越高越优质。\n我想修改这个标准可以吗？当然可以。\noptimize 提供了一个名为 maximize参数来修改优化标准。我可以将最大回撤或夏普比率作为优化标准。\n将优化标准改为最大回撤：\nbt.optimize( fast_ma_window=range(5, 30, 5), slow_ma_window=range(10, 60, 10), constraint=lambda p: p.fast_ma_window \u0026lt; p.slow_ma_window, maximize=\u0026#39;Max. Drawdown [%]\u0026#39;, ) 将优化标准改为夏普比率：\nbt.optimize( fast_ma_window=range(5, 30, 5), slow_ma_window=range(10, 60, 10), constraint=lambda p: p.fast_ma_window \u0026lt; p.slow_ma_window, maximize=\u0026#34;Sharpe Ratio\u0026#34;, ) 优化函数 # 这个优化标准默认是选设置的优化指标的最大值，如果想越小越好，可将传递优化指标名改为函数，自定义标准。\n如选择年化波动率最小的参数组合，示例代码：\nbt.optimize( fast_ma_window=range(5, 30, 5), slow_ma_window=range(10, 60, 10), constraint=lambda p: p.fast_ma_window \u0026lt; p.slow_ma_window, maximize=lambda r: -r[\u0026#34;Volatility (Ann.) [%]\u0026#34;], ) 通过匿名函数 maximize=lambda r: -r[\u0026quot;Volatility (Ann.) [%]\u0026quot;] 选择年化波动波动率最小的组合。\n不过，从我平时的使用体验来看，还是默认的 SQN 的优化结果比较合理，它的评价维度更加全面。\n自定义优化指标 # 除了那些内置的优化指标，我还可以自定义标准。举个例子，我现在想寻找 \u0026ldquo;在市场中持仓时间最短收益最大” 的参数组合。\n定义优化目标函数：\ndef equity_per_exposure(r): final_equity = r[\u0026#39;Equity Final [$]\u0026#39;] # 最终净值 exposure_time = r[\u0026#39;Exposure Time [%]\u0026#39;] # 持仓时间 return final_equity / exposure_time 目标函数是通过最终净值除以持仓时间比率计算得到，将其传递给 bt.optimize。\n注：如要准确计算 \u0026ldquo;持仓时间\u0026rdquo; 要用 持仓时间占比 * 总时间，这省略了，因为它不影响策略的评估结果。\nstats = bt.optimize( fast_ma_window=range(5, 30, 5), slow_ma_window=range(10, 60, 10), constraint=lambda p: p.fast_ma_window \u0026lt; p.slow_ma_window. maximize=equity_per_exposure, ) 如果用这个指标优化策略，得到的参数组合可能交易次数很少，因为这样容易出现持有时间短，收益高的情况。这不是我想要的。\n如何解决这个问题？\n还可以在优化函数中添加条件约束，确保得到参数组合是的合性。在函数中加上如果交易次数小于 50 次，返回 -1，保证它们不会被优化结果选中。\ndef equity_per_exposure(series): if series[\u0026#39;# Trades\u0026#39;] \u0026lt; 50: return -1 # 返回负值以避免选择 final_equity = series[\u0026#39;Equity Final [$]\u0026#39;] exposure_time = series[\u0026#39;Exposure Time [%]\u0026#39;] return final_equity / exposure_time 好像通过交易次数约束不太合理，那还可以基于单位时间交易次数过滤，如交易频率 100 天要至少交易 1 次。\ndef equity_per_exposure(series): if r[\u0026#34;# Trades\u0026#34;] / r[\u0026#34;Duration\u0026#34;].days \u0026lt; 0.01: return -1 final_equity = series[\u0026#39;Equity Final [$]\u0026#39;] exposure_time = series[\u0026#39;Exposure Time [%]\u0026#39;] return final_equity / exposure_time 通过自定义优化指标，可以更灵活地定义你的目标，发现满足你需求的最佳参数组合。\n参数优化热力图 # 参数优化热力图是一种直观的方式来比较不同参数组合对策略表现的影响。通过可视化参数范围内的结果，可以快速识别最佳参数区域。\nBacktesting.py 中支持生成热力图的方法。\n生成基础热力图 # 在优化函数 optimize 中要启用热力图选项 return_heatmap。\nstats, hm = bt.optimize( fast_ma_window=range(5, 50, 5), slow_ma_window=range(10, 100, 10), constraint=lambda p: p.fast_ma_window \u0026lt; p.slow_ma_window, return_heatmap=True ) 优化函数 bt.optimize 返回了两个对象，其中的第二个对象是 hm 就是绘制热力图的数据，包含了所有参数组合的优化结果。\n打印下 hm 变量。\nprint(hm) 输出：\nfast_ma_window slow_ma_window 5 10 1.792704 20 2.703298 30 2.392251 40 2.257054 50 1.794700 10 20 2.643361 30 1.938471 40 2.427356 50 1.603184 15 20 1.832693 30 1.720452 40 1.762611 50 1.365671 20 30 1.316673 40 2.230364 50 1.389500 25 30 1.178816 40 2.128967 50 0.915063 Name: SQN, dtype: float64 参数组合对应的值就是优化指标的值，因为我的 optimize 没有指定 maximize，默认值就是 SQN。\n有了数据后，就可以用 Backtesting.py 内置函数 plot_heatmaps 绘制热力图了。\nfrom backtesting.lib import plot_heatmaps 粗略查看，表现最好区域在黄色区域，大概是 fast_ma_window 位于 [5, 10], slow_ma_window 位于 [20, 30, 40] 区域；\n多参数组合的热力图 # 现在的 SMACrossStrategy 策略只有两个参数，如果是三个或更多参数，plot_heatmaps 会将参数两两组合绘制每个组合的热力图。\n简单修改下 SMACrossStrategy 的逻辑，加上 ATR 止损，通过一个 atr_factor 控制止损的乘数，如下所示：\nclass SMACrossStrategy(Strategy): # 参数 fast_ma_window = 10 slow_ma_window = 20 atr_factor = 3 def init(self): self.fast_ma = self.I( talib.SMA, self.data.Close, timeperiod=self.fast_ma_window ) self.slow_ma = self.I( talib.SMA, self.data.Close, timeperiod=self.slow_ma_window ) self.atr = self.I( talib.ATR, self.data.High, self.data.Low, self.data.Close, timeperiod=14 ) def next(self): if self.fast_ma[-1] \u0026gt; self.slow_ma[-1] and not self.position: self.buy(sl=self.data.Close[-1] - self.atr_factor * self.atr[-1]) elif self.fast_ma[-1] \u0026lt; self.slow_ma[-1] and self.position.is_long: self.position.close() 同时参数优化加上 atr_factor：\nstats, hm = bt.optimize( fast_ma_window=range(5, 30, 5), slow_ma_window=range(10, 60, 10), atr_factor=range(1, 4), constraint=lambda p: p.fast_ma_window \u0026lt; p.slow_ma_window, return_heatmap=True, ) 将新得到的 hm 交给 plot_heatmaps 绘制。\nplot_heatmaps(hm, agg=\u0026#39;mean\u0026#39;) 其中有个 agg 参数，它指定了分组聚合方式，默认为 max，这里修改为 mean，即采用平均值聚合。\n新生成的热力图：\n如果参数过多，组合数量会急剧增加，且大部分的组合（如 atr_factor 和 fast_ma_window）没有分析价值，我们可以通过 pandas 分组聚合提取想要的参数组合。\n示例代码：\nhm = hm.groupby(by=[\u0026#34;fast_ma_window\u0026#34;, \u0026#34;slow_ma_window\u0026#34;]).mean() plot_heatmaps(hm) 注意点\n热力图适用于分析两个参数间的关系，如果参数过多，图表数量会急剧增加。 使用合理的参数范围和步长可以减少计算量并提高可读性。 更灵活的使用方法，可分组聚合关心的参数组合进行分析。 通过查看参数优化热力图，可以高效地探索参数空间，不仅能找到表现最佳的策略组合，还能减少选出过拟合参数组合的可能性。\n总结 # 本文介绍了如何在 Backtesting.py 中进行参数优化，包括基础的网格搜索、参数约束、自定义优化指标以及通过热力图可视化优化结果等内容。通过这些方法，可以有效提升策略的表现，同时避免无意义的参数组合。热力图为分析参数间的关系提供了直观工具，是优化策略的重要辅助。掌握这些技巧，可以更高效地开发和改进交易策略。\n希望本文对你所有帮助。\n","date":"2024-12-24","externalUrl":null,"permalink":"/posts/2024-12-18-backtestingpy-guide-part2/","section":"文章","summary":"上篇文章介绍了如何使用 Backetsting.py 快速上手。今天继续介绍另一个策略回测时非常重要的点，参数优化。参数优化是提升策略表现的重要步骤，而 Backtesting.py 内置了参数优化功能，使用起来还是很方便的。\n","title":"Backtesting.py 参数优化入门","type":"posts"},{"content":"在算法交易中，验证一个策略是否有效至关重要，但该如何验证呢？\n我们可以通过模拟盘或直接实盘测试策略，但能看到的问题有限。最快捷有效的方式是基于历史数据回测（Backtesting），模拟策略在过去市场中的表现，在评估收益和风险后，再进行模拟和实盘。\n那该如何回测呢？\n回测的方式有很多，既可以手工测试、也可以借助 excel 或是 python 的向量计算库，如 numpy 和 pandas 进行。不过这些方式都比较原始，我们可使用市面一些现成的回测框架，不仅能尽可能避免回测中常见的问题，还能把重点放在策略的逻辑上。\n本文将先介绍 Backtesting.py，一个 Python 实现轻量级、事件驱动的回测框架。之所以先介绍它，主要是它非常简单。\n什么是 Backtesting.py # Backtesting.py 是一个轻量级的 Python 回测框架，专注于策略回测的核心功能，适合快速实现和测试交易策略。与其他回测框架，如 VectorBT 或 Backtrader 等回测框架相比，Backtesting.py 简化了使用流程，没有过多复杂功能，自然地，它更加简单易用与高效。\nBacktesting.py 内置了策略参数优化工具，可与 Pandas 数据框和 NumPy 数组兼容，易于集成。当然简单是有代价的，它的缺点是不支持多资产交易和分数股交易，这也限制了它的普适性，一般被用于 CTA 策略的回测。\n让我们快速上手 Backtesting.py 的使用吧。\n安装 # 确保你已经有了 Python 环境，使用如下命令安装 backtesting.py 和 TA-Lib 两个依赖包。\npip install backtesting TA-Lib 如果 TA-Lib 安装遇到困难，请查看 TA-Lib 安装指南，或者也可以选择其他技术指标库，如 pandas-ta。\n编写第一个策略 # 开始通过 backtesting.py 实现第一个策略，就以均线交叉策略为例。策略的规则也很简单，就是 SMA 金叉开仓买入、死叉平仓。暂时只考虑做多的情况。\n导入必要的模块 # 首先，创建一个名为 strategy.py 的 Python 文件，开始编写我们的策略。\n导入要用到的模块：\nfrom backtesting import Backtest, Strategy from backtesting.test import GOOG # 示例数据 import talib 简单起见，我们就用 Backtesting.py 提供的样例数据 GOOG，即 Google 某段时间点的价格数据来演示。\n定义策略类 # 我们定义一个名为 SMACrossStrategy 策略类，继承 Strategy：\nclass SMACrossStrategy(Strategy): # 参数 fast_ma_window = 10 slow_ma_window = 20 def init(self): # 初始化阶段：计算指标 self.fast_ma = self.I(talib.SMA, self.data.Close, timeperiod=self.fast_ma_window) self.slow_ma = self.I(talib.SMA, self.data.Close, timeperiod=self.slow_ma_window) def next(self): # 交易阶段：逻辑判断和执行交易 if self.fast_ma[-1] \u0026gt; self.slow_ma[-1] and not self.position: self.buy() elif self.fast_ma[-1] \u0026lt; self.slow_ma[-1] and self.position.is_long: self.position.close() 在 Backtestingpy 中实现的策略要继承 backtesting 下 Strategy 类，实现它的两个方法：init 和 next。Strategy 是框架提供的基类，它规定了策略结构和生命周期，init 方法用于初始化，如指标的向量计算，next 中用于实现提供数据的每个 bar 的执行逻辑。\n策略类上的两个变量 fast_ma_window 和 slow_ma_window 是我们定义的策略参数，代表了均线的快线和慢线的周期，默认设置为 10 和 20。它们是可配置的，在调用策略时，是可以修改这两个参数的值的。\n在 init 初始化阶段，我们可以通过 talib 计算 SMA 均线的快慢线。数据可以通过 self.data 访问，指标计算用到了收盘价，即 self.data.Close。\n在 next 交易逻辑阶段，只需要拿到当前的指标数据，判断是否要入场就行了，self.fast_ma[-1] 和 self.slow_ma[-1] 分别代表了最新的快线和慢线的值，当 fast_ma 大于 slow_ma 且当前没有仓位（not self.position) 执行买入，否则执行平仓。\n交叉 crossover # 如果你希望策略的判断严格是金叉死叉的那一刻，不仅仅要比较 -1，即当前的指标，还有比较 -2，即上个周期的大小。\n金叉，即快线 fast_ma 上穿慢线 slow_ma：\nfast_ma[-1] \u0026gt; slow_ma[-1] and fast_ma[-2] \u0026lt; slow_ma[-2] 死叉，即快线 fast_ma 下穿慢线 slow_ma，也就是慢线 slow_ma 上穿快线 fast_ma：\nfast_ma[-1] \u0026lt; slow_ma[-1] and fast_ma[-2] \u0026gt; slow_ma[-2] 好在 Backtesting.py 提供了一个函数，专门用于判断这种两条线交叉的场景，即 backtesting.lib 下 crossover(s1, s2) 函数，如果 s1 上穿 s2，则返回 True，否则返回 False。\nfrom backtesting.lib import crossover # 金叉 crossover(fast_ma, slow_ma) # 死叉 crossover(slow_ma, fast_ma) 现在就简洁多了。\n运行回测 # 我们要创建 Backtest 实例运行回测，将 Google 数据，策略类、初始金额和交易费率传递 Backtest 完成实例化。\n示例代码：\nbt = Backtest(GOOG, SMACrossStrategy, cash=10000, commission=0.002) stats = bt.run() 现在运行代码，bt.run() 的返回值 stats 就是我们要的回测结果。\nprint(stats) 输出评测结果，如下：\nStart 2004-08-19 00:00:00 # 开始时间 End 2013-03-01 00:00:00 # 结束时间 Duration 3116 days 00:00:00 # 总持续时间 Exposure Time [%] 61.545624 # 资金暴露时间比例（%） Equity Final [$] 99485.0574 # 最终净值（美元） Equity Peak [$] 100607.2574 # 最高净值（美元） Return [%] 894.850574 # 总收益率（%） Buy \u0026amp; Hold Return [%] 703.458242 # 买入持有策略收益率（%） Return (Ann.) [%] 30.934891 # 年化收益率（%） Volatility (Ann.) [%] 32.215003 # 年化波动率（%） Sharpe Ratio 0.960263 # 夏普比率（风险调整收益） Sortino Ratio 2.125336 # 索提诺比率（下行风险调整收益） Calmar Ratio 1.497421 # 卡尔玛比率（收益与最大回撤之比） Max. Drawdown [%] -20.658779 # 最大回撤（%） Avg. Drawdown [%] -3.678412 # 平均回撤幅度（%） Max. Drawdown Duration 584 days 00:00:00 # 最大回撤持续时间 Avg. Drawdown Duration 38 days 00:00:00 # 平均回撤持续时间 # Trades 48 # 总交易次数 Win Rate [%] 64.583333 # 胜率（%） Best Trade [%] 57.11931 # 最佳单笔交易收益率（%） Worst Trade [%] -12.446769 # 最差单笔交易收益率（%） Avg. Trade [%] 4.923007 # 平均单笔交易收益率（%） Max. Trade Duration 121 days 00:00:00 # 最长持仓时间 Avg. Trade Duration 39 days 00:00:00 # 平均持仓时间 Profit Factor 4.729158 # 盈亏比（获利交易总额与亏损交易总额之比） Expectancy [%] 5.66852 # 期望收益率（平均每笔交易的收益率） SQN 2.643361 # 系统质量数（策略表现综合评分） _strategy SMACrossStrategy... # 策略名称（移动均线交叉） _equity_curve ... # 净值曲线 _trades Size EntryB... # 交易详情 dtype: object 如上是策略统计数据，有包括初始资金、最终净值、最大回撤等，还有其他评价策略表现的指标，如最大回撤、夏普比率、胜率等。\n如果想查看回测的收益曲线，Backtest 提供方法 plot 查看可视化结果：\nbt.plot() 运行上述代码后，你将看到：\n图表中显示了策略的收益曲线、买卖点、指标曲线和我们重点的几个交易统计数据，如最终净值、最高净值、最大回撤等。\n除了使用默认参数外，我们可以在调用 bt.run() 修改默认参数：\nbt.run(fast_ma_window=15, slow_ma_window=30) 在这不到 30 行代码里，我们就实现了一个简单的均线交叉策略，可见 Backtesting.py 的简洁高效。\n自定义数据 # Backtesting.py 是允许使用自定义数据的。\n要求格式 # 数据要求是 pandas.DataFrame，且满足按时间升序排列，时间为 DataFrame 的索引，同时数据列至少包含 Open、High、Low、Close、Volume 五个字段即可。\n查看样例数据格式；\nprint(GOOG) 输出：\nOpen High Low Close Volume 2004-08-19 100.00 104.06 95.96 100.34 22351900 2004-08-20 101.01 109.08 100.50 108.31 11428600 2004-08-23 110.75 113.48 109.05 109.40 9137200 2004-08-24 111.24 111.60 103.57 104.87 7631300 2004-08-25 104.96 108.00 103.88 106.00 4598900 ... ... ... ... ... ... 2013-02-25 802.30 808.41 790.49 790.77 2303900 2013-02-26 795.00 795.95 784.40 790.13 2202500 2013-02-27 794.80 804.75 791.11 799.78 2026100 2013-02-28 801.10 806.99 801.03 801.20 2265800 2013-03-01 797.80 807.14 796.15 806.19 2175400 示例：适配 tushare # 演示下将 tushare 数据转为 Backtesting.py 支持的格式吧。从 tushare 获取南华商品期货黄金指数的价格。\n示例代码：\npro = ts.pro_api() df = pro.index_daily(ts_code=\u0026#34;AU.NH\u0026#34;, start_date=\u0026#34;2024-01-01\u0026#34;) df[\u0026#34;trade_date\u0026#34;] = pd.to_datetime(df[\u0026#34;trade_date\u0026#34;]) df.set_index(\u0026#34;trade_date\u0026#34;, inplace=True) df.index.name = \u0026#34;Datetime\u0026#34; df.sort_index(inplace=True) df.rename( columns={ \u0026#34;open\u0026#34;: \u0026#34;Open\u0026#34;, \u0026#34;high\u0026#34;: \u0026#34;High\u0026#34;, \u0026#34;low\u0026#34;: \u0026#34;Low\u0026#34;, \u0026#34;close\u0026#34;: \u0026#34;Close\u0026#34;, \u0026#34;vol\u0026#34;: \u0026#34;Volume\u0026#34;, }, inplace=True, ) df = df[[\u0026#34;Open\u0026#34;, \u0026#34;High\u0026#34;, \u0026#34;Low\u0026#34;, \u0026#34;Close\u0026#34;, \u0026#34;Volume\u0026#34;]] print(df.head()) 输出：\nOpen High Low Close Volume Datetime 2024-01-02 1805.2399 1813.4864 1802.4661 1811.6872 95373.0 2024-01-03 1810.1883 1812.0918 1803.8779 1809.4987 187356.0 2024-01-04 1799.2201 1804.1197 1792.3877 1802.4116 243384.0 2024-01-05 1802.8461 1808.0885 1799.5808 1805.1527 142594.0 2024-01-08 1801.9185 1813.1337 1796.4979 1802.6821 295652.0 回测图表保存 # Backtesting.py 提供了保存回测图表的功能，便于后续分析。\n保存 HTML 文件 # 我们上面运行回测时，Backtesting.py 会在当前目录下自动生成一个 HTML 文件，即上面展示的回测图表。\n默认的文件名称是 \u0026ldquo;策略类名.html\u0026rdquo;，。\n示例：\nSMACrossStrategy.html 如果是参数优化得到的结果，文件名会带上参数：\nSMACrossStrategy(fast_ma_window=5,slow_ma_window=20).html 这个名称在调用 bt.plot 时可以修改。\nbt.plot(filename=\u0026#39;results/plot.html\u0026#39;) 上述代码将回测结果保存在 results 目录下。如果目录不存在，需要提前创建：\nmkdir -p results 动态命名文件 # 我们也可以动态设置文件名以区分不同的参数组合。例如：\nfast = stats._strategy.fast_ma_window slow = stats._strategy.slow_ma_window bt.plot( filename=f\u0026#39;results/fast{fast}_slow{slow}.html\u0026#39;, ) 这将保存文件名如 fast10_slow20.html 的 HTML 文件，方便管理批量的回测结果。\n总结 # 本文介绍了 Backtesting.py 回测框架的快速上手使用，从策略创建、回测与结果保存。下篇文章介绍 Backtesting.py 的参数优化部分，这是策略开发是非常重要的一个点。\n","date":"2024-12-18","externalUrl":null,"permalink":"/posts/2024-12-12-backtestingpy-guide-part1/","section":"文章","summary":"在算法交易中，验证一个策略是否有效至关重要，但该如何验证呢？\n我们可以通过模拟盘或直接实盘测试策略，但能看到的问题有限。最快捷有效的方式是基于历史数据回测（Backtesting），模拟策略在过去市场中的表现，在评估收益和风险后，再进行模拟和实盘。\n","title":"Backtesting.py 快速上手","type":"posts"},{"content":"油管是很多人获取知识和资讯的渠道，有很多有价值的信息。对于要深入学习的内容，肯定要看完整个视频，而如果你只是关注一些资讯或分析视频，跟进热点事件，比如我平时回看一些财经币圈资讯，花时间看完每个视频，效率低下。油管上有不少高质量的英文视频，看起来有点吃力，转为中文，理解起来更容易。\n于是，我想实现一个小工具，自动检测我关注频道的更新，发现就下载整理成文章发给我。\n现在市面上有没有这样的工具呢？有些 AI 工具，要进到视频播放页面分析总结，但我很赖，压根不想去到某个频道查看它是否有新视频更新。我没有找到，或许即使有，也是付费产品。\n这篇文章我将实现个小工具，监控指定频道，把监控到的最新视频提取成文章，推送给我。我将用 Python 完成这一系列工作。\n实现流程 # 先把实现流程明确下来，这个工具要实现以下功能：\n频道检测：检查感兴趣油管频道是否有新的视频发布； 下载音频：检测到新视频后，只下载它的音频部分，节省存储空间； 提取字幕：提取音频内容中的文字； 生成文章：将提取的字幕整理为通俗易懂的文章； 这样，我就不需要逐个观看每个视频，直接阅读整理后的文章即可，如果是很感兴趣的视频，可以深入观看。\n按流程把代码结构明确下来：\nimport click @click.command() @click.option(\u0026#34;--channel-url\u0026#34;, type=click.STRING, help=\u0026#34;Channel URL\u0026#34;) @click.option(\u0026#34;--count\u0026#34;, default=5, type=click.INT, help=\u0026#34;Video Count\u0026#34;) @click.option(\u0026#34;--output-dir\u0026#34;, default=\u0026#34;.\u0026#34;, type=click.STRING, help=\u0026#34;Output Directory\u0026#34;) def main(channel_url, count, output_dir): # 检测频道 videos = fetch_latest_videos(channel_url, count) for video in videos: # 下载音频 audio_path = download_audio(video[\u0026#34;url\u0026#34;], output_dir=output_dir) # 识别字幕 subtitles = transcribe_audio(audio_path) # 调用 openai 生成文章 article = generate_article(audio_path, subtitles, video[\u0026#34;title\u0026#34;], video[\u0026#34;url\u0026#34;]) print(article) 这里其实还少了一个推送的动作，支持自动邮件推送和 git 上传，这个用 python 实现也很简单。\n用到哪些工具？ # 为了实现这个流程，还需要几个重要的 Python 库：\nyt-dlp：一个强大的 YouTube 下载工具，可以获取视频元数据或音频； Whisper：OpenAI 的音频转文字模型，用于从音频中提取字幕文字； OpenAI：用来调用 OpenAI 的接口，将识别出来的字幕转换为通俗易懂的文章； 如下命令安装依赖包：\npip install yt-dlp openai-whisper openai 开始具体的实现吧！我将按步骤进行。\n监控油管频道 # 油管频道页面会列出了最新发布的视频，通过 yt-dlp 工具来抓取这些信息。\n代码示例：\nimport yt_dlp def fetch_latest_videos(channel_url, fetch_count=5): ydl_opts = { \u0026#34;quiet\u0026#34;: True, \u0026#34;extract_flat\u0026#34;: \u0026#34;in_playlist\u0026#34;, \u0026#34;playlist_items\u0026#34;: f\u0026#34;1-{fetch_count}\u0026#34;, } with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(channel_url, download=False) return [{\u0026#34;title\u0026#34;: entry[\u0026#34;title\u0026#34;], \u0026#34;url\u0026#34;: entry[\u0026#34;url\u0026#34;]} for entry in info[\u0026#34;entries\u0026#34;]] 这段代码会提取频道最新的视频列表，包括标题和链接。我们只提取元信息，并不下载视频内容。其中的 fetch_count 是用来指定获取最新视频的数量。\n执行命令监听某个频道：\npython main.py --channel-url https://www.youtube.com/@CoinBureau/videos 输出最新的 5 个视频：\ntitle:Wall Street Elites Are Going To Pump Crypto: Find Out Who!!, url: https://www.youtube.com/watch?v=WQCCwC2-Wkw title:Is Trump Media The Next MicroStrategy!? TruthFi, Bakkt, \u0026amp; More!!, url: https://www.youtube.com/watch?v=c2-Gf473kmQ title:APT to 10x?! Aptos Updates \u0026amp; Predictions You CANT Miss!!, url: https://www.youtube.com/watch?v=n39P_F6Cx58 title:Why Europe Is Falling Apart—and What It Means for YOU, url: https://www.youtube.com/watch?v=qB2QaYXXoBo title:Crypto’s Dark Secret Exposed: Why 75% of Investors FAIL!, url: https://www.youtube.com/watch?v=-ZNXm3nX3zM 下载音频文件 # 拿到了更新的视频链接后，我们用 yt-dlp 下载音频文件。只下载音频是为了节省空间，简单高效。\n函数 download_audio 实现代码：\ndef download_audio(video_url, output_dir): ydl_opts = { \u0026#34;format\u0026#34;: \u0026#34;bestaudio/best\u0026#34;, \u0026#34;postprocessors\u0026#34;: [ { \u0026#34;key\u0026#34;: \u0026#34;FFmpegExtractAudio\u0026#34;, \u0026#34;preferredcodec\u0026#34;: \u0026#34;mp3\u0026#34;, \u0026#34;preferredquality\u0026#34;: \u0026#34;192\u0026#34;, } ], \u0026#34;outtmpl\u0026#34;: f\u0026#34;{output_dir}/%(id)s/audio.%(ext)s\u0026#34;, \u0026#34;download_archive\u0026#34;: \u0026#34;downloaded_videos.txt\u0026#34;, # 防止重新下载 } with yt_dlp.YoutubeDL(ydl_opts) as ydl: info_dict = ydl.extract_info(video_url, download=True) if info_dict is not None and \u0026#34;requested_downloads\u0026#34; in info_dict: return info_dict[\u0026#34;title\u0026#34;], info_dict[\u0026#34;requested_downloads\u0026#34;][0][\u0026#34;filepath\u0026#34;] else: return f\u0026#34;{output_dir}/{video_url.split(\u0026#39;v=\u0026#39;)[-1]}/audio.mp3\u0026#34; 这个函数的功能包括：\n实现音频部分的下载，将下载文件统一转换为 MP3 格式； 输出文件会保存在指定目录中，目录名为为视频 ID； 下载函数的返回值为下载音频的文件路径； 支持去重下载，下载过的音频，会将 video id 保存到 download_videos.txt； 执行脚本就可以下载指定频道的音频文件了。\n$ python main.py --channel-url https://www.youtube.com/@CoinBureau/videos 提取字幕 # 下载了音频文件后，通过 OpenAI 的 Whisper 模型就可以从音频中提取文本字幕。whisper 的使用很简单。\ntranscribe_audio 的实现代码：\nimport os import whisper def transcribe_audio(audio_path): subtitle_path = audio_path.replace(\u0026#34;audio.mp3\u0026#34;, \u0026#34;subtitles.txt\u0026#34;) if os.path.exists(subtitle_path): with open(subtitle_path) as fd: return fd.read() model = whisper.load_model(\u0026#34;base\u0026#34;) result = model.transcribe(audio_path) subtitles = result[\u0026#34;text\u0026#34;] with open(subtitle_path, \u0026#34;w+\u0026#34;) as fd: fd.write(subtitles) return subtitles 我们使用 Whisper 的 base 模型识别音频，它的返回 result[text] 就是音频的完整字幕文本。上面的代码为了防止重复识别，加了去重判断逻辑，如果已识别字幕，直接访问文字即可。\n注：如果首次使用 whisper 识别音频，它会先下载模型文件，可能高达几个 G 的大小。\n生成文章 # 有了字幕，现在就可以借助 GPT 模型将器整理成逻辑清晰、易于阅读的文章。\nimport openai ai_client = openai.OpenAI() def generate_article(audio_path, subtitles, title, video_url): article_path = audio_path.replace(\u0026#34;audio.mp3\u0026#34;, \u0026#34;article.md\u0026#34;) if os.path.exists(article_path): with open(article_path) as fd: return fd.read() response = ai_client.chat.completions.create( model=\u0026#34;gpt-4o\u0026#34;, messages=[ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;You are a helpful assistant.\u0026#34;}, { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;以下是视频的字幕内容：\\n\\n{subtitles}\\n\\n请根据这些字幕生成一篇文章，原始主题是：{title}\u0026#34;, }, ], temperature=0.7, ) article = response.choices[0].message.content article = f\u0026#34;来源：[{title}]({video_url})\\n\\n{article}\u0026#34; with open(article_path, \u0026#34;w+\u0026#34;) as fd: fd.write(article) return article 将字幕内容和提示词交易 gpt 模型，最终就能得到我们心心念念的最终结果。\n$ python main.py --channel-url https://www.youtube.com/@CoinBureau/videos 摘取生成文章的开头部分，如下：\n来源：[Wall Street Elites Are Going To Pump Crypto: Find Out Who!!](https://www.youtube.com/watch?v=WQCCwC2-Wkw) # 华尔街精英将推动加密货币市场：看看是谁在背后！ 近年来，加密货币市场一直不乏看涨的催化剂。但如果我告诉你，全球金融界最强大的力量之一正悄悄准备在2025年大规模撼动市场，你会怎么想？这个力量就是与华尔街精英同义的Cantor Fitzgerald。 ## Cantor Fitzgerald的崛起与发展 Cantor Fitzgerald成立于1945年，由Bernard Gerald Cantor和John Fitzgerald创立。最初这是一家不起眼的投资银行，但很快演变为华尔街的强大力量。在早期，Cantor涉足大型区块销售和机构投资者的股票交易，为其未来的成功奠定了基础。1972年，公司成为全球首个美国政府证券的电子市场，这一里程碑让其在金融技术创新领域声名鹊起。在Howard Lutnik于1991年成为Cantor的首席执行官后，公司加倍致力于技术创新。 注意点 # openai 是要 APIKEY 的，我已经设置了环境变量，关于 openai 权限，需要各位自己想办法。 如果字幕太大，openai 没有办法直接处理，可以考虑切分字幕，或将直接丢弃，毕竟转化太大的视频也是很浪费钱的。 以什么形式将文章推送给你，可以是邮件或其他形式，每个人的场景不一定相同。 定时触发的能力，我们可以用 crontab 或者 python 的 schedule 库实现。 自动生成的文章可能存在一些拼写错误，这是由于 whisper 字幕识别导致的错误，不过不影响我们阅读。 总结 # 一个简单小工具就实现对 YouTube 频道的自动监控，快速获取视频字幕并生成结构化中文文章，减少不必要的时间浪费，从而提升信息获取的效率。\n我要做个赖人。\n希望这篇文章有点小作用。\n","date":"2024-12-15","externalUrl":null,"permalink":"/posts/2024-12-15-monitor-youtube-channel/","section":"文章","summary":"油管是很多人获取知识和资讯的渠道，有很多有价值的信息。对于要深入学习的内容，肯定要看完整个视频，而如果你只是关注一些资讯或分析视频，跟进热点事件，比如我平时回看一些财经币圈资讯，花时间看完每个视频，效率低下。油管上有不少高质量的英文视频，看起来有点吃力，转为中文，理解起来更容易。\n","title":"借助 OpenAI 开发一个效率小工具监控油管频道","type":"posts"},{"content":"mplfinance 是一个基于 matplotlib 的金融数据可视化工具，旨在方便地生成 OHLC、K线图和其他金融相关图表。\n本文将带你逐步从基础到高级使用，帮助你熟练掌握 mplfinance 的使用技巧。\n安装 mplfinance # 在使用 mplfinance 前，需要先安装它。可以通过以下命令安装：\npip install --upgrade mplfinance mplfinance 依赖于 matplotlib 和 pandas，确保你的环境中已安装这些库。\n准备数据 # 在开始绘图之前，需要准备一个包含 OHLC 数据的 Pandas DataFrame。\n我们从 Tushare 下载股票的历史数据：\nimport pandas as pd import datetime import tushare as ts pro = ts.pro_api() data = pro.daily(ts_code=\u0026#34;000001.SZ\u0026#34;, start_date=start_date.strftime(\u0026#34;%Y%m%d\u0026#34;)) data[\u0026#34;trade_date\u0026#34;] = pd.to_datetime(data[\u0026#34;trade_date\u0026#34;]) data.set_index(\u0026#34;trade_date\u0026#34;, inplace=True) data.index.name = \u0026#34;Date\u0026#34; data = data[[\u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;, \u0026#34;vol\u0026#34;]] data.rename( columns={ \u0026#34;open\u0026#34;: \u0026#34;Open\u0026#34;, \u0026#34;high\u0026#34;: \u0026#34;High\u0026#34;, \u0026#34;low\u0026#34;: \u0026#34;Low\u0026#34;, \u0026#34;close\u0026#34;: \u0026#34;Close\u0026#34;, \u0026#34;vol\u0026#34;: \u0026#34;Volume\u0026#34;, }, inplace=True, ) data.sort_index(ascending=True, inplace=True) 数据示例如下：\nOpen High Low Close Volume Date 2023-12-12 9.31 9.50 9.30 9.42 855016.42 2023-12-13 9.38 9.39 9.15 9.16 1061301.65 2023-12-14 9.21 9.28 9.15 9.15 742900.56 2023-12-15 9.20 9.35 9.19 9.21 988939.42 2023-12-18 9.18 9.24 9.09 9.13 654425.62 只保留接下来会用到的数据列。\n绘制基础图表 # 现在，我们只要将数据导入 mplfinance 即可。\n示例代码如下，调用 plot 方法，并传入从 tushare 得到的数据生成图表：\nimport mplfinance as mpf # 绘制基础 K 线图 mpf.plot(data, volume=True) 你将看到一个简单的 OHLC 图表，显示指定时间范围内的开盘、收盘、最高和最低价。参数 volume 设置为 True 表示显示成交量。\n默认的图标类型按 ohlc 美国线风格显示，接着往下看，如何将其修改为蜡烛线。\n更改图表类型 # mplfinance 提供多种图表类型，包括默认的 ohlc（美国线）、candle（K 线图）、line（折线图）、renko（砖型图） 和 pnf（点数图） 等。\n示例：\nmpf.plot(data, type=\u0026#39;candle\u0026#39;, volume=True) 其他图标类型：\nmpf.plot(data, type=\u0026#39;line\u0026#39;, volume=True) mpf.plot(data, type=\u0026#39;renko\u0026#39;, volume=True) mpf.plot(data, type=\u0026#39;pnf\u0026#39;, volume=True) 添加移动平均线（MA） # mplfinance 支持了对均线的支持，可以通过 mav 参数添加单个或多个移动平均线。\n如添加单条移动平均线，示例：\nmpf.plot(data, type=\u0026#39;ohlc\u0026#39;, mav=4) 或添加多条移动平均线，示例：\nmpf.plot(data, type=\u0026#39;candle\u0026#39;, mav=(3, 6, 9)) 其他指标 # 除了均线，我们也可以在图表中叠加自己的技术指标，如 EMA，还有布林带。\n可以通过 talib 计算指标，示例如下：\nema20 = ta.EMA(data[\u0026#34;Close\u0026#34;], timeperiod=10) upper, middle, lower = ta.BBANDS( data[\u0026#34;Close\u0026#34;], timeperiod=20, nbdevup=2, nbdevdn=2, matype=0 ) plots = [ mpf.make_addplot(ema20, label=\u0026#34;EMA10\u0026#34;), mpf.make_addplot(upper, label=\u0026#34;BB_UPPER\u0026#34;), mpf.make_addplot(middle, label=\u0026#34;BB_MIDDLE\u0026#34;), mpf.make_addplot(lower, label=\u0026#34;BB_LOWER\u0026#34;), ] mpf.plot( data, type=\u0026#34;candle\u0026#34;, addplot=plots, ) 如上的示例中，只添加了图例，除此以外，make_addplot 还可以设置其他属性，如 color，type 之类的。\nmplfinance 的代码库中中提供了大量的代码示例，想深入了解这个库，建议多看看这些案例，访问 examples。\n保存图表 # 绘制完成后，可以将图表保存为图片文件：\nmpf.plot(data, type=\u0026#39;candle\u0026#39;, volume=True, savefig=\u0026#39;output.png\u0026#39;) 动画与实时更新 # mplfinance 支持动态更新图表，适合实时显示金融数据。通过 animation 功能，可以生成动态图表，可参考官方文档中的示例：Animation/Updating plots。这块就不展开介绍了，因为如果确有动态更新的需求，lightweight-charts 比 mplfinance 更合适。\n总结 # mplfinance 是一个功能强大且易于使用的金融数据可视化工具，从基础的 K 线图到高级的技术分析图表都能轻松实现。\n以下是几点建议：\n先熟悉基础功能：学会加载数据并绘制简单图表。 逐步扩展技巧：尝试添加移动平均线、成交量及自定义指标。 参考官方教程：多浏览示例代码和文档。 希望这篇教程能帮你快速上手 mplfinance！\n","date":"2024-12-10","externalUrl":null,"permalink":"/posts/2024-12-11-mplfinance-chart-library/","section":"文章","summary":"mplfinance 是一个基于 matplotlib 的金融数据可视化工具，旨在方便地生成 OHLC、K线图和其他金融相关图表。\n本文将带你逐步从基础到高级使用，帮助你熟练掌握 mplfinance 的使用技巧。\n","title":"mplfinance - 一个轻松绘制股票行情图表的 python 库","type":"posts"},{"content":"在做数据分析或时，有没有碰到过这样的情况：数据明明下载下来了，却发现了一些问题。比如有的数据缺失，有的地方重复了，还有的怎么看都觉得异常。这个时候，你可能会想：“这数据还能用吗？”\n今天，就来聊聊数据清洗和预处理，用一个常见的股票行情数据作为例子，走一遍实际的操作。你会发现，这并不是一件多难的事。\n数据清洗是什么？为什么重要？ # 简单来说，数据清洗和预处理就是给你的原始数据“洗澡”，把里面的脏东西都清理干净。我们要检查缺失值、重复值，甚至一些很怪的异常值。为什么要做这件事呢？很简单，因为“垃圾数据”会让分析结果一团糟，俗话说：“垃圾进，垃圾出（Garbage in, garbage out）。”\n如果你不想做了半天无用功，那就得花时间好好清理数据。\n用股票行情数据举个例子 # 假设我们从两个地方下载了同一只股票的每日收盘价数据，分别叫数据集 A 和数据集 B。看起来差不多，但仔细一看就发现问题多了去了：有缺失值、有重复记录，还有价格离谱的异常点。接下来，我们一步步清理。\n处理缺失值 # 我们发现数据集 A 中有些日期的收盘价是空的。这种情况怎么办呢？有两种简单的办法：\n删掉空行：如果缺失的数据不多，可以直接删掉。 填补空缺：比如用前一天的数据、平均值或者插值。 示例操作：\nimport pandas as pd import numpy as np # 模拟数据 data = { \u0026#39;Date\u0026#39;: [\u0026#39;2024-12-01\u0026#39;, \u0026#39;2024-12-02\u0026#39;, \u0026#39;2024-12-03\u0026#39;, \u0026#39;2024-12-04\u0026#39;], \u0026#39;Close Price\u0026#39;: [100, 101, np.nan, 103], } df = pd.DataFrame(data) 我们可以删除缺失值，如下：\ndf_cleaned = df.dropna() print(df_cleaned) 输出：\nDate Close Price 0 2024-12-01 100.0 1 2024-12-02 101.0 3 2024-12-04 103.0 或者用插值法填补缺失值，如下：\ndf_filled = df.interpolate() print(df_filled) 输出：\nDate Close Price 0 2024-12-01 100.0 1 2024-12-02 101.0 2 2024-12-03 102.0 3 2024-12-04 103.0 小提示：如果缺失值太多，可能就要重新找数据源了。\n去掉重复数据 # 如果我们的数据集中某些日期重复了多次，如同一天的收盘价被记录了两次。重复的数据会导致分析时重复计算，得出的结果可能偏差很大。\n通过如下代码可直接删除重复值：\ndata = { \u0026#39;Date\u0026#39;: [\u0026#39;2024-12-01\u0026#39;, \u0026#39;2024-12-01\u0026#39;, \u0026#39;2024-12-02\u0026#39;, \u0026#39;2024-12-03\u0026#39;, \u0026#39;2024-12-04\u0026#39;], \u0026#39;Close Price\u0026#39;: [100, 100, 101, np.nan, 103], } df = pd.DataFrame(data) df_cleaned = df.drop_duplicates() print(df_cleaned) 输出：\n0 2024-12-01 100.0 2 2024-12-02 101.0 3 2024-12-03 NaN 4 2024-12-04 103.0 检测异常值 # 如果某一天的价格突然跳到 500，而前一天才 100，这怎么看都不正常。我们可以通过检测价格的变化幅度来发现这些异常点。比如，如果价格波动超过 50%，就需要进一步检查。\n示例操作：\ndata = { \u0026#39;Date\u0026#39;: [\u0026#39;2024-12-01\u0026#39;, \u0026#39;2024-12-02\u0026#39;, \u0026#39;2024-12-03\u0026#39;, \u0026#39;2024-12-04\u0026#39;], \u0026#39;Close Price\u0026#39;: [100, 101, 500, 103], # 500 是异常值 } df = pd.DataFrame(data) # 检测价格波动是否异常 df[\u0026#39;Pct Change\u0026#39;] = df[\u0026#39;Close Price\u0026#39;].pct_change() df[\u0026#39;Anomaly\u0026#39;] = df[\u0026#39;Pct Change\u0026#39;].abs() \u0026gt; 0.5 print(df[df[\u0026#39;Anomaly\u0026#39;]]) # 打印异常值 输出：\nDate Close Price Pct Change Anomaly 2 2024-12-03 500 3.950495 True 3 2024-12-04 103 -0.794000 True 解决方法：如果确定是录入错误，可以用前一天的价格或者移动平均值替代。\n检查日期是否正确 # 股票市场一般只在工作日交易，周末和节假日是不会有数据的。所以我们需要检查数据中是否有周末的记录，或者日期格式是否一致。\n示例操作：\n# 转换日期格式并检查周末数据 df[\u0026#39;Date\u0026#39;] = pd.to_datetime(df[\u0026#39;Date\u0026#39;]) # 检查是否有周末数据 df[\u0026#39;Weekday\u0026#39;] = df[\u0026#39;Date\u0026#39;].dt.weekday print(df[df[\u0026#39;Weekday\u0026#39;] \u0026gt;= 5]) # 打印周末数据 可视化检查 # 如果你不确定哪里出了问题，可以试着把数据画成图表，这样异常点一眼就能看出来。比如我们可以绘制收盘价随时间的变化趋势。\n示例操作：\nimport matplotlib.pyplot as plt # 绘制收盘价趋势图 plt.plot(df[\u0026#39;Date\u0026#39;], df[\u0026#39;Close Price\u0026#39;], marker=\u0026#39;o\u0026#39;) plt.title(\u0026#39;Stock Closing Prices Over Time\u0026#39;) plt.xlabel(\u0026#39;Date\u0026#39;) plt.ylabel(\u0026#39;Close Price\u0026#39;) plt.show() 数据清洗的几点建议 # 从最基本的检查开始\n查找缺失值、重复值和格式问题。 确保日期、单位等字段一致。 借助可视化工具\n用图表直观地查看数据，快速发现问题。 多数据源验证\n如果一个数据源问题太多，可以参考其他数据源交叉验证。 记得定期更新数据\n金融数据尤其如此，时效性很重要。 总结 # 清洗数据就像是搭建房子的地基，虽然不是最吸引人的部分，但却是最重要的环节之一。通过处理缺失值、去掉重复数据、检测异常值等步骤，我们可以确保数据的准确性和可靠性。\n希望通过今天的案例，你能对数据清洗有更清晰的认识。下次拿到数据的时候，不妨试试这些方法，帮你打好分析的基础。\n","date":"2024-12-10","externalUrl":null,"permalink":"/posts/2024-12-10-financial-data-processing/","section":"文章","summary":"在做数据分析或时，有没有碰到过这样的情况：数据明明下载下来了，却发现了一些问题。比如有的数据缺失，有的地方重复了，还有的怎么看都觉得异常。这个时候，你可能会想：“这数据还能用吗？”\n","title":"数据清洗与预处理：做好分析的第一步","type":"posts"},{"content":"本文继续介绍如何获取金融数据。我将用 tushare 下载金融数据，主要还是通过最基础的数据介绍它的使用。\n什么是 Tushare？ # Tushare 是一个 Python 开源金融数平台，最初由 Jimmy 开发的，最早版 tushare 是借助爬虫从财经数据平台如新浪财经实时抓取数据。可查看 tushare.org，这是 tushare 早期的文档。\n2018 年，Tushare 经过了几年的发展，Tushare 开发了独立的数据平台-tushare.pro，从爬虫方案改为自建服务存储数据，数据质量上有了质的提升，让 Tushare Pro 成为了金融分析师和研究人员常用的金融数据源之一。\n我曾参与 Tushare 的开发工作，大概2018 下半年的半年时间，和 Tushare 作者 Jimmy 一起开发了 Tushare 的新平台，主要是构建了数据上线的流程，从零搭建了这个平台，为其提供了配置支持，简化了数据上线流程，加快了数据上线效率。\n安装与注册 # 首先，你需要安装 Tushare 的 Python 包，在终端中运行以下命令来安装 Tushare。\npip install tushare 然后，注册 tushare.pro，获取 API 密钥。这个过程我就不介绍了，可查看 tushare.pro 操作手册。\n验证是否安装成功：\nimport tushare as ts ts.set_token(\u0026#34;Your API Token\u0026#34;) pro = ts.pro_api() data = pro.daily(ts_code=\u0026#34;000001.SZ\u0026#34;) print(data) 输出：\nts_code trade_date open ... vol amount 0 000001.SZ 20241206 11.44 ... 1726269.27 2.004270e+06 1 000001.SZ 20241205 11.44 ... 687108.83 7.861380e+05 2 000001.SZ 20241204 11.44 ... 1007470.59 1.154623e+06 3 000001.SZ 20241203 11.37 ... 1082559.36 1.236993e+06 4 000001.SZ 20241202 11.39 ... 975433.66 1.108786e+06 ... ... ... ... ... ... ... 5995 000001.SZ 19990713 21.80 ... 305130.00 6.368993e+05 5996 000001.SZ 19990712 23.70 ... 269953.00 6.109031e+05 5997 000001.SZ 19990709 23.80 ... 139487.00 3.281077e+05 5998 000001.SZ 19990708 23.30 ... 156405.00 3.746743e+05 5999 000001.SZ 19990707 23.00 ... 244797.00 5.773778e+05 [6000 rows x 11 columns] 数据内容 # Tushare 作者米哥 Jimmy 本身是有专业金融数据商的深度经验，从 Tushare 的数据组织上就能看出来，非常清晰和完善，基本上你想要的数据，tushare pro 都有提供，完全不亚于专业数据提供商。\n沪深股票 指数 公募基金 期货 期权 债券 外汇 港股 美股 其他等等 积分费用 # Tushare Pro 上每个接口都有积分要求，股票的日线行情调用积分要求是 120 积分，不过只要注册和修改昵称就能得到这 120 积分，所以说这部分数据就是免费的。更高阶的数据的积分也会要求比较高，关于它如何获取积分，文档 平台积分 中有具体的描述。\ntushare 的积分可以通过邀请注册或者社区贡献获取，如多写几篇教程文章，其实难度不算高。如果不想浪费这个时间，积分也是通过捐助的方式获取，其实就是花钱买，100 块能得到 1000 积分，积分有效期一年。大概有 2000 积分，能调用 60% 的数据了，5000 积分的话，除了部分特色数据和高频数据，其余数据基本都能调用。这个费用不算高，我看过其他一些数据商，数据通常是分类售卖，1000 块也只能买到某个品类下的基础数据。\n总体而言，虽然说 tushare 有收费，不过相对于其他的数据提供商，还是降低了普通散户获取专业金融数据的门槛。\n开始介绍它的数据吧，我还是按照上篇介绍 akshare 的思路介绍如何在 tushare 获取 A 股的一些数据吧。\nA 股数据 # Tushare 提供了丰富的股票数据，包括历史行情、实时行情等，还提供了每日指标，方便我们选股。\n历史行情 # Tushare 提供了历史行情日/周/月线数据，包含了股票的开盘价、收盘价、最高价、最低价等信息，还有如。\n示例代码：\n# 获取某只股票（如平安银行）的历史日线数据 pro.daily(ts_code=\u0026#39;000001.SZ\u0026#39;, start_date=\u0026#39;20230101\u0026#39;, end_date=\u0026#39;20231231\u0026#39;) # 获取某只股票（如平安银行）的历史周线数据 pro.weekly(ts_code=\u0026#39;000001.SZ\u0026#39;, start_date=\u0026#39;20180101\u0026#39;, end_date=\u0026#39;20181101\u0026#39; # 获取某只股票（如平安银行）的历史月线数据 pro.monthly(ts_code=\u0026#39;000001.SZ\u0026#39;, start_date=\u0026#39;20180101\u0026#39;, end_date=\u0026#39;20181101\u0026#39; daily 的积分要求是 120，weekly 和 monthly 要更高的积分才能调用。不过这两个接口，我们是可以通过 pandas 提供的 resample 将日线聚合成周线和月线，可以不用调用这两个接口。\n如果你有更高频的数据要求，如分钟线，这块是付费数据。Tushare Pro 封装了一个 pro_bar 的通用行情接口，其中直接不同品类、不同频率的行情数据。\n如获取分钟数据，示例如下：\nts.pro_bar(ts_code=\u0026#39;000001.SZ\u0026#39;, adj=\u0026#39;qfq\u0026#39;, freq=\u0026#39;1\u0026#39;, start_date=\u0026#39;20180101\u0026#39;, end_date=\u0026#39;20181011\u0026#39;) 实时行情 # Tushare 的实时行情不是自建服务，就是提供了其他平台的转发。在 Tushare 中提供了便于使用接口。\nts.realtime_quote(ts_code=\u0026#39;600000.SH,000001.SZ,000001.SH\u0026#39;) 输出：\nNAME TS_CODE DATE TIME OPEN PRE_CLOSE PRICE ... A2_P A3_V A3_P A4_V A4_P A5_V A5_P 0 浦发银行 600000.SH 20231222 15:00:00 6.570 6.570 6.580 ... 6.590 1834 6.600 4107 6.610 2684 6.620 1 平安银行 000001.SH 20231222 15:00:00 9.190 9.170 9.200 ... 9.210 2177 9.220 2568 9.230 2319 9.240 2 上证指数 000001.SH 20231222 15:30:39 2919.2879 2918.7149 2914.7752 ... 0 0 0 0 财报数据 # Tushare 提供了上市公司财报数据，包括资产负债表、利润表和现金流量表。\n示例代码：\n# 资产负债表 pro.balancesheet(ts_code=\u0026#39;000001.SZ\u0026#39;) # 利润表 pro.income(ts_code=\u0026#34;000001.SZ\u0026#34;) # 现金流量表 pro.cashflow(ts_code=\u0026#39;000001.SZ\u0026#39;) # 分红送股 pro.dividend(ts_code=\u0026#39;000001.SZ\u0026#39;) 除此以外，还提供了财务指标数据，便于进行选股。\npro.fina_indicator(ts_code=\u0026#34;000001.SZ\u0026#34;) 说到财务指标，其实 Tushare Pro 的技术指标因子，如果你不想算的，可以从如下接口获取。\npro.stk_factor(ts_code=\u0026#34;000001.SZ\u0026#34;) 宏观经济 # Tushare 提供了多种宏观经济数据，涵盖了 GDP、PMI、利率等重要经济指标。\nGDP 数据 # 示例代码：\npro.cn_gdp(start_q=\u0026#34;2018Q1\u0026#34;, end_q=\u0026#34;2019Q3\u0026#34;) 输出：\nquarter gdp gdp_yoy pi pi_yoy si si_yoy ti ti_yoy 0 2019Q3 712845.4 6.2 43005.0 2.9 276912.5 5.6 392927.9 7.0 1 2019Q2 460636.7 6.3 23207.0 3.0 179122.1 5.8 258307.5 7.0 2 2019Q1 218062.8 6.4 8769.4 2.7 81806.5 6.1 127486.9 7.0 3 2018Q4 900309.5 6.6 64734.0 3.5 366000.9 5.8 469574.6 7.6 4 2018Q3 646710.9 6.7 39799.8 3.4 261822.8 5.8 345088.2 7.7 5 2018Q2 417215.4 6.8 21576.2 3.2 168558.1 6.1 227081.1 7.6 6 2018Q1 197920.0 6.8 8574.4 3.2 77116.7 6.3 112229.0 7.5 PMI 数据 # PMI（采购经理人指数）反映了制造业经济的活跃程度。\n示例代码：\n# 获取最新的PMI数据 pro.cn_pmi(start_m=\u0026#39;201901\u0026#39;, end_m=\u0026#39;202003\u0026#39;, fields=\u0026#39;month,pmi010000,pmi010400\u0026#39;) 利率数据 # Tushare 提供了中国央行的利率数据，帮助分析货币政策的变化。\n示例代码：\n# 获取贷款基础利率 pro.shibor_lpr(start_date=\u0026#39;20180101\u0026#39;, end_date=\u0026#39;20181130\u0026#39;, fields=\u0026#39;date,1y\u0026#39;) 其他还有cpi、ppi 等，就不一一演示了，可查看文档 宏观经济-国内宏观。\n特色数据 # Tushare 还提供了如新闻联播文字稿、实时新闻资讯、甚至是新冠疫情的数据。\n如新闻资讯数据，帮助我们及时了解股市、经济等领域的最新动态。\n示例代码：\n# 获取最新的股市新闻资讯 pro.news() 输出：\ndatetime content title 0 2024-12-09 14:14:57 【东吴证券：维持宇通客车“买入”评级，预计公司产销景气度将继续提升】东吴证券研报指出，宇通客... None 1 2024-12-09 14:13:18 【收购资产不及预期？股价盘中一度跌停，国网信通最新回应】国网信通低开低走，盘中一度触及跌停。... None 2 2024-12-09 14:12:21 【高盛：《燕云十六声》将是网易-S在2025年最重要的游戏发布和贡献之一】高盛发布研究报告称... None 3 2024-12-09 14:11:58 【招银国际：重申途虎-W“买入”评级 目标价升至26港元】招银国际发布研究报告称，重申途虎-... None 4 2024-12-09 14:10:36 国债期货午后冲高回落，30年期主力合约涨幅收窄至0.09%，10年期主力合约跌0.01%，5... None .. ... ... ... 995 2024-12-08 20:58:39 伊朗外交部：德黑兰继续支持国际机制，特别是联合国安理会第2254号决议，以推进叙利亚的政治进程。 None 996 2024-12-08 20:57:45 伊朗外交部声明称，伊朗呼吁迅速结束军事冲突，防止恐怖主义行动，并在叙利亚社会各界的参与下开始... None 997 2024-12-08 20:57:38 【也门胡塞武装：与伊拉克民兵武装联合袭击以色列南部目标】当地时间12月8日，也门胡塞武装表示... None 998 2024-12-08 20:56:25 伊朗外交部声明称，伊朗尊重叙利亚的统一和国家主权；叙利亚的前途和命运完全由叙利亚人民决定。 None 999 2024-12-08 20:45:39 约旦国王阿卜杜拉：敦促避免叙利亚发生任何可能导致混乱的冲突。 None [1000 rows x 3 columns] 总结 # Tushare 是一个强大的数据平台，主要适合于中国市场，包括 A 股、期货、期权等。通过简单的 API 调用，你可以轻松获取各类金融数据，并进行分析与研究。如果你正在寻找一个免费且高质量的金融数据源，Tushare 无疑是一个不错的选择。\n有兴趣的话，还可以到它文档中搜寻一番，看看是否新的有趣数据是，或许能发现一些有意思的数据。据我之前的了解，Tushare 的作者米哥对挖掘新数据特别感兴趣。\n","date":"2024-12-04","externalUrl":null,"permalink":"/posts/2024-11-04-financial-data-using-tushare/","section":"文章","summary":"本文继续介绍如何获取金融数据。我将用 tushare 下载金融数据，主要还是通过最基础的数据介绍它的使用。\n什么是 Tushare？ # Tushare 是一个 Python 开源金融数平台，最初由 Jimmy 开发的，最早版 tushare 是借助爬虫从财经数据平台如新浪财经实时抓取数据。可查看 tushare.org，这是 tushare 早期的文档。\n","title":"从 Tushare 数据下载金融数据","type":"posts"},{"content":"研究交易策略时，高质量数据是分析和构建交易策略的根本。你可能会遇到如下的一些问题。\n数据从哪找？ 数据怎么清理？ 数据又该如何维护？ 本文将先开始介绍第一个主题，即 \u0026ldquo;数据从哪里找？\u0026quot;，我将首先基于 Python 开源数据包 akshare 下载数据，为什么选择作为第一个数据源？因为它是完全免费的。\n常见分类 # 在研究和构建策略时，基于目标不同，对数据的要求也不同。不同的数据适合不同的策略。找到合适的数据集，能显著提升策略。\n市场价格数据，即开高低收和交易量数据，如果构建 CTA 策略，价格数据已经足够了；\n公司财务数据，财务三大报表，如资产负债表、损益表，可助我们评估股票的长期投资价值，作为选股因子等；\n宏观经济数据，比如 GDP、通胀率、就业情况或是针对不同行业的数据等，有助于评估整体经济、观周期或某个行业的前景等；\n其他另类数据，也开始逐渐被重视，如像新闻、社交媒体信息等文本信息，重要人物的社交账号等，有助于我们及时抓住短期的波动；\n这篇文章主要演示如上的这些数据的下载方法。\n还想细化分类的话，不同角度还可以有不同分类，如：\n从品种上，有股票、期货、期权、外汇、债券和基金的不同品种上的区别，如股票有财报数据，而期货可能更加关注供需分析；非场内基金的价格数据就没有了开高低收；期权还包含独特的衍生数据，即它的隐含波动率和各类希腊字母；如果你关注数字货币市场，还要关注一些链上的活动，如跟踪巨鲸的账户，识别链上地址的活跃度，大额转账。\n从频率上，有小时线日线周线月线，还有高频数据，如分钟级、秒级甚至 tick 分笔的数据。对于如趋势追踪等 CTA 策略只要日线小时线级别即可，高频交易则要分析分钟或订单薄的分笔数据，这类高频数据的数据量会非常大，开源数据不会提供，购买的费用也相对贵一些。\n注：我在淘宝上发现有卖高频数据，价格一般几百块，因为数据量巨大，还送硬盘，不过我没有买，也不知道真假。\n数据的种类繁多，上面是一些常规分类。不同的交易策略，对数据的要求还是有很大区别，如你分析的是一些特殊事件对行业的影响，如美联储主席讲话对行情的实时影响，可能需要从视频提取字幕与行情相映射，还有如新冠疫情的影响，要收集每日的疫情数据，分析它和行情的关系。\n选择数据源 # 对于数据源方面，我选择开源数据，如 akshare、tushare 和 yfinance。\nakshare 是一个金融数据的 Python 包，数据种类繁多，主要是通过爬虫实时抓取网上的公开数据，提供国内股票、期货、外汇、宏观经济等多领域的金融数据接口，可以说，它的数据非常庞杂。\ntushare，和 akshare 一样，是专注于国内市场，虽然它有积分要求，但积分的获取难度不高，如果是购买积分，500/年就能使用 90% 的数据。它的数据是单独维护管理的，毫无疑问，数据质量很高的，在性价比这块，没有其他数据源可比。\nyfinance 是一个用于从 Yahoo Finance 获取金融数据的 Python 库。主要是国外市场，可下载如股票、期货、外汇等资产的历史市场数据，包括价格、交易量、公司财务数据等。\n如果你已经有可用的 Python 环境，安装过程就非常简单，如下命令安装这三个 Python 包：\npip install akshare pip install tushare pip install yfinance 如果没有 Python 环境，可以去了解下 anaconda，用它安装我们的 Python 环境。\n数据下载 # 本文目标是先通过 akshare 实现 Python 下载 A 股的数据，毕竟它是完全免费的，门槛最低。我会通过代码快速一览 akshare 获取这些数据的函数。而对于 tushare 和 yfinance，准备另外的篇章介绍。\n还有，文中也不会提供数据样例，毕竟是太占用空间了。\n先提前导入 Python 包：\nimport akshare as aks 市场价格 # A 股股票：包括它的历史行情、实时行情和分笔数据的获取。\n历史行情的接口示例：\n# 日线/周线/月线 aks.stock_zh_a_hist(symbol=\u0026#34;000001\u0026#34;, start_date=\u0026#34;20200101\u0026#34;, end_date=\u0026#34;20241201\u0026#34;, period=\u0026#34;daily\u0026#34;, # daily/weekly/monthly adjust=\u0026#34;qfq\u0026#34;) # qfq-前复权/hfq-后复权/默认不复权 # 日线 aks.stock_zh_a_daily(symbol=\u0026#34;sz000001\u0026#34;, start_date=\u0026#34;20200101\u0026#34;, end_date=\u0026#34;20241201\u0026#34;, adjust=\u0026#34;qfq\u0026#34;) # 分钟线 aks.stock_zh_a_minute(symbol=\u0026#34;sz000001\u0026#34;, period=\u0026#34;1\u0026#34;, # 1-1分钟/5-5分钟/15-15分钟/30-30分钟/60-1小时 adjust=\u0026#34;qfq\u0026#34;) 如上的代码演示了获取 A 股市场日线/周线/月线/小时/分钟级别的历史行情数据的函数。\n日线数据示例：\n日期 股票代码 开盘 收盘 最高 最低 成交量 成交额 振幅 涨跌幅 涨跌额 换手率 0 2020-01-02 000001 14.77 14.99 15.07 14.67 1530232 2.571196e+09 2.75 2.88 0.42 0.79 1 2020-01-03 000001 15.06 15.30 15.43 15.04 1116195 1.914495e+09 2.60 2.07 0.31 0.58 2 2020-01-06 000001 15.13 15.19 15.46 15.03 862084 1.477930e+09 2.81 -0.72 -0.11 0.44 3 2020-01-07 000001 15.25 15.27 15.40 15.07 728608 1.247047e+09 2.17 0.53 0.08 0.38 4 2020-01-08 000001 15.12 14.78 15.17 14.75 847824 1.423609e+09 2.75 -3.21 -0.49 0.44 因为 akshare 的数据是从直接财经网站拉取的，不同站的 symbol 规则有所差异，如 stock_zh_a_hist 是东方财富的接口，symbol 为 000001 即可，而其余两个接口是 sina 新浪财经，要加上交易所前缀：深交所-sz和上交所-sh。\n还有一点，我测试发现，分钟到小时级别的数据接口只能返回有限的数据，不是全量。不过这个也还好，如果确有需求，可以通过其他渠道拿到历史全量数据，通过这个接口获取增量数据。\n实时行情的接口示例：\naks.stock_zh_a_spot() 输出示例：\n代码 名称 最新价 涨跌额 涨跌幅 买入 卖出 昨收 今开 最高 最低 成交量 成交额 时间戳 0 bj430017 星昊医药 15.35 -0.36 -2.292 15.32 15.35 15.71 15.86 15.87 15.28 1922319.0 29838001.0 15:30:01 1 bj430047 诺思兰德 13.57 -0.51 -3.622 13.56 13.57 14.08 14.12 14.18 13.44 3269916.0 45133670.0 15:30:01 2 bj430090 同辉信息 7.56 -0.27 -3.448 7.56 7.57 7.83 7.89 8.01 7.53 7122586.0 54781060.0 15:30:01 3 bj430139 华岭股份 28.69 -1.87 -6.119 28.68 28.69 30.56 30.26 30.59 28.52 8799534.0 258453787.0 15:30:01 4 bj430198 微创光电 13.38 -0.36 -2.620 13.37 13.38 13.74 13.62 14.08 13.00 4379609.0 59129424.0 15:30:01 这个接口返回的是全部股票的实时行情数据，请求耗时较长，确实在可用性不怎么好，tushare 上也有实时行情接口 realtime_quote，同样是从各个财经站点上抓取的，不过封装的更好。\n当日的分笔数据的接口示例：\naks.stock_zh_a_tick_tx_js(symbol=\u0026#34;sz000001\u0026#34;) 这个接口返回的是某个股票当日的每笔成交列表。\nA 股指数： 以沪深 300 为例，演示 akshare 获取股票指数的历史和实时行情的函数。\n历史行情的演示示例：\n# 日线 aks.stock_zh_index_daily(symbol=\u0026#34;sh000300\u0026#34;) # sina aks.stock_zh_index_daily_tx(symbol=\u0026#34;sh000300\u0026#34;) # tencent aks.stock_zh_index_daily_em(symbol=\u0026#34;sh000300\u0026#34;) # 东方财富 这个接口会直接拉取指定指数的全部日线行情。\n实时行情的演示示例：\naks.stock_zh_index_spot_sina() # sina aks.stock_zh_index_spot_em() # 东方财富 这两个接口都是获取当前的实时行情，不过它返回的是一个全部数据的 pd.Dataframe，没有提供 symbol 参数指定指数 Symbol，要简单过滤才能拿到指定的 symbol 数据。\n搞到这里，你会发现 akshare 没有在易用性投入太多处理。如果你查看它的数据接口列表，它更多还是一个数据大杂烩，很多数据都能找到。但受限于全部来源于公开数据，没有存储下来，更像是给我提供了数据采集渠道，我们还有更多的工作要做。\n财务数据 # A 股三大报表：\naks.stock_financial_report_sina(stock=\u0026#34;sz000001\u0026#34;, symbol=\u0026#34;资产负债表\u0026#34;) aks.stock_financial_report_sina(stock=\u0026#34;sz000001\u0026#34;, symbol=\u0026#34;利润表\u0026#34;) aks.stock_financial_report_sina(stock=\u0026#34;sz000001\u0026#34;, symbol=\u0026#34;现金流量表\u0026#34;) 如资产负载表数据示例：\n报告日 资产 现金及存放中央银行款项 其中:现金 存放中央银行款 结算准备金 ... 数据源 是否审计 公告日期 币种 类型 更新日期 0 20240930 NaN 281777000000.0 NaN NaN NaN ... 定期报告 未审计 20241019 CNY 合并期末 2024-10-18T18:20:11 1 20240630 NaN 305998000000.0 NaN NaN NaN ... 定期报告 未审计 20240816 CNY 合并期末 2024-08-15T20:35:13 2 20240331 NaN 322076000000.0 NaN NaN NaN ... 定期报告 未审计 20240420 CNY 合并期末 2024-04-19T19:15:02 3 20231231 NaN 274663000000.0 NaN NaN NaN ... 定期报告 是 20241019 CNY 合并期末 2024-03-14T19:05:04 4 20230930 NaN 350143000000.0 NaN NaN NaN ... 定期报告 未审计 20231025 CNY 合并期末 2023-10-24T20:40:01 [5 rows x 150 columns] 数据列非常的多，这里面只展示了部分。\n这个接口通过 symbol 指定名称获取不同的财报，这也是挺奇怪。不得不吐槽下，易用性上和 tushare 不好比。\n其它常用数据还有如业绩预告、分红送股等数据。\n# 业绩预告 aks.stock_yjyg_em(date=\u0026#34;20240930\u0026#34;) # 从 20081231 开始，日期必须是 20200331 20200630 20200930 20201231 # 分红送股 aks.stock_history_dividend_detail(\u0026#34;000001\u0026#34;) 财务数据大概就找这么多吧。\n一句题外话，akshare 有些数据有限，可以通过其他渠道下载全量数据，然后再用 akshare 维护增量数据。毕竟是免费更香，虽然要更多的时间成本。\n宏观数据 # 宏观数据方面，主要看如何获取如 GDP、CPI、PMI、PPI、利率和就业数据等。\naks.macro_china_gdp() # GDP 国内生产总值 aks.macro_china_cpi() # CPI 居民消费价格指数 aks.macro_china_pmi() # PMI 采购经理人指数 aks.macro_china_ppi() # PPI 工业品出厂价格指数 aks.macro_china_lpr() # LRP 贷款报价利率 aks.macro_china_urban_unemployment() # 城镇调查失业率 如国内生产总值 GDP 的数据样例：\n季度 国内生产总值-绝对值 国内生产总值-同比增长 第一产业-绝对值 第一产业-同比增长 第二产业-绝对值 第二产业-同比增长 第三产业-绝对值 第三产业-同比增长 0 2024年第1-3季度 949745.7 4.8 57733.1 3.4 361361.6 5.4 530651.1 4.7 1 2024年第1-2季度 616836.0 5.0 30660.0 3.5 236529.9 5.8 349646.1 4.6 2 2024年第1季度 296299.5 5.3 11538.4 3.3 109846.3 6.0 174914.7 5.0 3 2023年第1-4季度 1260582.1 5.2 89755.2 4.1 482588.5 4.7 688238.4 5.8 4 2023年第1-3季度 912692.0 5.2 56330.1 4.0 348885.6 4.4 507476.2 6.0 akshare 提供了各类宏观数据，相对更加丰富。除了国内的宏观经济数据，还有中国香港和其他不少国家的宏观数据，如美国、日本、欧洲等一些国家。\n我本人不是专业金融出生，先整理下这些数据，等日后慢慢研究，以求为自己的交易增加更多理解。\n总结 # 本文介绍了如何从 akshare 获取常用的数据，提供了使用代码。如果想查看它提供的其他接口函数，可查看 akshare 接口文档。\n不得不说，akshare 提供的数据丰富，是收集公开数据的一个利器，免去了我们自行寻找数据的时间。\n","date":"2024-11-01","externalUrl":null,"permalink":"/posts/2024-11-02-financial-market-data/","section":"文章","summary":"研究交易策略时，高质量数据是分析和构建交易策略的根本。你可能会遇到如下的一些问题。\n数据从哪找？ 数据怎么清理？ 数据又该如何维护？ 本文将先开始介绍第一个主题，即 “数据从哪里找？\"，我将首先基于 Python 开源数据包 akshare 下载数据，为什么选择作为第一个数据源？因为它是完全免费的。\n","title":"通过 python 获取金融数据-akshare","type":"posts"},{"content":"本文想基于我的简单理解说说什么是算法交易，或者说是量化交易。\n什么是算法交易？ # 刚开始接触算法交易的时候，对它的理解，它就是把我平时的交易规则搬进计算机里自动执行。这个理解也没错，算法交易就是用计算机程序去执行一系列明确的交易规则。\n一个简单的交易算法要包含有如：\n什么时候买？ 什么时候卖？ 止损是多少？ 等等。\n一旦确认了规则，我们将其转化为程序交给计算机去执行即可。\n如果你已经熟练掌握如何开发一个算法交易程序，时间更多会被耗在如何找到一个能赚钱的策略。这个研究过程就需要一套方法论了，本身并不容易。如果没有这个过程，你设计的规则极有可能是一个稳定亏钱的策略。\n为什么要算法交易？ # 不同于人工操作，算法交易不需要全天候盯盘。一旦设定了规则，它就会不停息地在后台运行，能省下我们不少精力去做其他事情。\n具体展开说说算法交易的优势，我觉得有三大点。\n提高交易速度和效率 在行情剧烈波动时，手动交易难免会出现反应慢半拍的情况。手动交易常会来不及点买卖键，眼睁睁地看着机会溜走。算法交易能在毫秒级完成交易决策，尤其在高频交易里，速度就是决定成败的关键。\n避免情绪干扰 手动交易常常会因为市场波动导致情绪反复，跌多了害怕，涨多了想追，这个真的很可怕。\n算法交易完全可避免情绪问题，只要设定好了规则，绝不会临阵退缩，毅然决然会按照规则执行。避免了我们被情绪左右，长期来看效果反而更好。\n稳健的风险管理 算法交易里可以设定各种风险管理的条件，比如最大亏损限制、止盈止损条件等。这相当于给交易过程套上了“安全锁”。\n之前手动操作时，我偶尔会因为没来得及止损而遭遇更大的亏损，而算法交易则让我不再担心这类问题，系统会在价格触及设定时点后自动平仓实现风险控制。\n扩大交易策略的应用范围 算法交易可以同时使用多种策略，覆盖多个市场或资产。我可以设定不同的策略，让它们各自执行不同的任务，去挖掘更多的机会，这样既能分散风险，也能提高收益率。\n算法交易策略分类 # 一般而言，算法交易策略有如下几个大类：\n趋势跟踪策略 这种策略就是俗话说的“顺势而为”。\n当市场有明确的趋势时，跟随趋势的策略往往表现不错。如市场在上涨的时候系统自动买入，在下跌时系统会自动卖出。趋势跟踪的策略在牛市或熊市中比较有效，不过在震荡市中可能会出现较多的“假信号”。\n均值回归策略 均值回归策略就是“超买卖出、超卖买入”。\n常常可用于均值回归的指标有布林带和RSI等指标，市场偏离平均水平时，就采取反向操作。这类策略可用于震荡市效果或者在大周期使用。\n套利策略 套利策略的本质是“低买高卖”，是通过不同市场或品种间的价格差异实现低风险套利。\n交易的两者要具有强相关性，如股指期货和现货股票、不同交易所间的套利。这种策略对计算要求较高，但如果找到市场间的价格不一致，就有机会赚取稳健的收益。\n除了以上低风险的套利，也有相对高风险的套利策略，如基于跨月合约的统计套利等。\n高频交易策略 高频交易可以说是“割草机”，靠超短期波动赚钱。它在市场中寻找细微的价差，通过极高的速度进行大量小额交易。高频交易对速度要求非常高，要配合高性能硬件，靠大量交易产生收益优势。\n机器学习与AI策略 基于机器学习算法来分析市场走势，这些算法基于历史数据和统计模型，可以识别市场中的潜在模式，甚至会随着市场变化“自我学习”。实现起来复杂，要通过数据挖掘建模，找到优势。\nAI 策略似乎越来越流行，不过因为能力有限，这个方向我还一直没有涉入，希望有能力早点涉入吧。\n当然，除此以外，还有其他的策略分类，这里就不一一介绍了。\n如何实现算法交易？ # 要开始算法交易，其实不需要多高的门槛，现在有不少实现算法交易的工具和编程平台，选择合适的工具能让交易过程变得顺利高效。\n介绍几种常见的实现方式：\nPython：适合有编程能力的交易员 Python 是目前算法交易中最流行的编程语言之一。\nPython 语法简洁，而且有丰富的数据处理和金融交易库（如 Pandas、NumPy、Matplotlib、TA-Lib 等），适合用来开发和测试自己的策略。\n如果喜欢自由度高、可定制性强的实现方式，那么 Python 是个好选择。这种方式对编程能力有一定要求，需要自己进行数据处理、策略测试和交易接口的对接。\n如果你的目标是高频交易，那么 Python 可能就不太适合，选择类似 C++、Rust 这类高性能语言。\n专业软件：如文化财经、MT5、MultiCharts等 对于不想自己编写代码或只需要简单策略的人来说，文化财经、MT5、MultiCharts 都是很好的选择。\n这些软件可用于期货、外汇、股票等不同的市场，提供了图表分析、自动化交易和策略测试功能。不过可能要学习它们内置的编程语言，编写和测试自动交易程序。\n如 MultiCharts 是一款支持多市场、多资产交易的软件平台，用户可以利用内置的 EasyLanguage 编写算法交易策略。它的优势在于界面友好、数据处理能力强，支持股票、期货、外汇等多个市场。MultiCharts 还支持图表交易、模拟交易等功能，适合想要同时参与多个市场的交易者。\n这些软件通常功能齐全强大，能节省交易维护的成本。当然，它们通常不是免费使用的。\n此外，还有一些 web 平台，如米筐、聚宽这些量化研究平台，不过，它们重在策略研究分析。\n开源交易系统：如 vn.py 和 backtrader 对于专业交易者和量化研究人员，如果确实不想付费，也可以选择一些开源解决方案，如 vn.py 和 backtrader 就是两个不错的选择。\nvn.py 是一个基于 Python 的开源量化交易框架，不仅仅可用于国内的期货和股票市场，它还支持多市场的扩展接口，基本市面上的交易接口都有支持。它也是我现在主要的交易工具。\nbacktrader 则是一个 Python 的开源回测库，主要用于历史数据回测，适合用来进行量化策略研究。不过，它也可以用实盘交易，只需要适当地扩展。\n但因为是开源方案，对使用者的要求肯定比付费软件要高。如我在使用 vnpy 的过程中，偶尔会发现一些不符合我预期的问题，要修改源码。\n结语 # 总的来说，算法交易帮能解决了很多实际操作中的\u0026quot;痛点\u0026quot;：速度、纪律性，减少情绪干扰，算法交易都能让交易更冷静高效。但我们要投入更多时间去设计和优化规则，且不是一劳永逸的，但一旦运转起来，它能节省我们很多时间，提高效率。\n算法的实现方式，要看个人的编程水平和交易需求。如果追求高自由度、个性化设置，Python 是首选。如果希望能直接使用，文化财经、MT5 和 MultiCharts 等专业软件就更合适。而 Web 平台则适合不想安装软件、希望随时随地交易的人。\n","date":"2024-11-01","externalUrl":null,"permalink":"/posts/2024-11-01-algorithmic-trading/","section":"文章","summary":"本文想基于我的简单理解说说什么是算法交易，或者说是量化交易。\n什么是算法交易？ # 刚开始接触算法交易的时候，对它的理解，它就是把我平时的交易规则搬进计算机里自动执行。这个理解也没错，算法交易就是用计算机程序去执行一系列明确的交易规则。\n","title":"浅谈算法交易","type":"posts"},{"content":"","date":"2024-10-10","externalUrl":null,"permalink":"/docs/gofyne/","section":"教程","summary":"","title":"Fyne 中文教程","type":"docs"},{"content":" 译文原文 理解未平仓量在期权交易中至关重要，它有助于做出明智的决策并制定成功的交易策略。未平仓量代表市场中特定执行价格和到期日的未关闭或未行权的期权合约总数。\n本文将深入探讨期权交易中的未平仓量的概念，分析其重要性、解读方式、实际应用等内容。此外，我们将演示如何使用Python来分析和解读未平仓量数据，为交易者提供有价值的见解和工具，以助力决策。\n什么是期权交易中的未平仓量？ # 期权交易中的未平仓量代表已经建立但仍未关闭或尚未行权的合约数量。与衡量特定时期内交易合约数量的成交量不同，未平仓量提供了市场对特定期权合约的兴趣深度。\n期权交易中未平仓量的关键概念：\n总合约数：未平仓量代表了当前交易者和投资者持有的期权合约总数。 未结算合约：这些合约包括已开立（买入或卖出）但尚未平仓闭或行权的合约。 市场兴趣：未平仓量帮助交易者和投资者衡量市场情绪及对特定期权合约的整体兴趣水平。 理解未平仓量对于分析市场趋势、识别潜在价格波动并制定有效的期权交易策略至关重要。\n期权交易中未平仓量的实际例子 # 接下来，我们来看一些未平仓量在期权交易中的实际例子。\n我们从2023-2024年的新闻以及财报发布的例子中找到一些实际参考。\n期权交易中的未平仓量：例子1 - 特斯拉股票（TSLA）的看涨期权和未平仓量 # 假设你对特斯拉（TSLA）股票感兴趣，并考虑购买看涨期权。特斯拉最近宣布了一项新的电池技术，分析师认为这将显著提升公司的未来表现。你猜测股价将在未来几个月内上涨。\n未平仓量分析：当你查看TSLA不同到期日和执行价格的未平仓量时，可能得出一些结论。假设在这个例子中，TSLA执行价格为1000美元、到期时间为3个月的看涨期权未平仓量在过去一周内显著增加。\n解读：未平仓量的增加表明越来越多的交易者在购买TSLA看涨期权。这可能表明 看涨情绪增加 和 期权活动增加。\n看涨情绪增加，许多交易者押注TSLA股价上涨，支持你的看涨分析。 期权活动增加，未平仓量的增加可能仅反映更多交易者进入了TSLA期权市场，但非表明明确的市场方向。 期权交易中的未平仓量：例子2 - 苹果公司（AAPL）的财报发布 # 苹果（AAPL）即将发布季度财报，你不确定在财报发布后股价会升还是会降。尝试进行未平仓量分析。你可能观察到即将到期的AAPL期权中，看涨和看跌期权的未平仓量显著增加。\n从而可能解读，看涨和看跌期权未平仓量的增加表明市场对财报结果的不确定性。交易者通过购买看涨期权（预期股价上涨）和看跌期权（预期股价下跌）进行对冲。\n你的决策，由于未平仓量信号的矛盾，仅依赖未平仓量可能不是理想选择。最好将此信息与技术分析或财报预测相结合，以做出更明智的决策。\n接下来，我们将继续探讨期权交易中未平仓量的类型。\n期权交易中未平仓量的类型 # 在期权交易中，未平仓量可以根据其表现分为三种主要类型：\n未平仓量增加 # 当某一特定期权的未平仓合约数量增加时，这意味着新头寸被建立。 未平仓量增加表明市场对该期权表现出兴趣，暗示看涨或看跌情绪，具体取决是看涨还是看跌期权。 未平仓量减少 # 当某一特定期权的未平仓合约数量减少时，表明现有头寸被关闭。 未平仓量减少可能表明交易者信心下降，暗示市场犹豫不决或趋势反转。 未平仓量稳定 # 当某一特定期权的未平仓合约数量在一段时间内保持相对不变时，表明市场情绪并未发生显著变化。 未平仓量的稳定可能暗示当前趋势可能持续，或表明潜在突破或下跌前的整合期。 理解未平仓量的表现对于分析市场情绪和做出明智的交易决策至关重要。通过监控未平仓量的变化，交易者可以获得潜在价格波动和市场趋势的重要洞察。\n接下来我们将了解期权交易中未平仓量的重要性，探讨为什么未平仓量备受关注。\n期权交易中未平仓量的重要性 # 未平仓量在期权交易中的重要性在于它能够提供市场情绪、流动性和潜在价格波动的重要见解。\n以下是未平仓量重要的几个关键原因：\n市场情绪分析： # 未平仓量反映了市场中未结算的期权合约数量。 未平仓量增加表明投资者对某个期权的兴趣增加，暗示潜在的看涨或看跌情绪。 未平仓量减少可能表明投资者兴趣减弱，暗示市场犹豫不决。 流动性衡量： # 高未平仓量表明某个期权具有较高的流动性，交易者可以更轻松地进入和退出头寸。低未平仓量表明流动性较低，可能导致较大的买卖差价和更高的交易成本。\n价格趋势识别： # 未平仓量的变化可以帮助交易者识别潜在的趋势反转或延续。 价格上涨伴随未平仓量增加可能表明趋势的加强，而未平仓量减少伴随价格上涨可能预示潜在的趋势反转。 接下来，我们将讨论未平仓量和价格之间的关系，以进一步加深对这一主题的理解。\n未平仓量与价格的关系 # 在未平仓量和价格之间存在三种可能的关系：\n正相关关系：未平仓量增加伴随价格上涨，可能表明趋势的加强，暗示投资者兴趣增长和潜在的趋势延续。 负相关关系：未平仓量减少伴随价格上涨，可能预示潜在的趋势反转，暗示投资者兴趣下降和潜在的趋势疲软。 支撑与阻力的指示：某个执行价格的高未平仓量可能对标的资产的价格形成吸引力，帮助在到期临近时识别关键的支撑和阻力位。 接下来我们使用Python展示未平仓量在期权交易中的应用。\n使用Python分析期权交易中的未平仓量 # 步骤1：导入库 # 首先导入用于数据处理和可视化的Python库。\n# 导入库 import pandas as pd import matplotlib.pyplot as plt 步骤2：获取数据并打印 # 读取包含三月和四月SBIN合约的CSV文件，并打印每个数据框的尾部以检查数据。\n# 读取数据 SBIN_March_Contract = pd.read_csv(\u0026#34;SBIN_March_Contract.csv\u0026#34;, index_col=0) SBIN_April_Contract = pd.read_csv(\u0026#34;SBIN_April_Contract.csv\u0026#34;, index_col=0) # 显示三月合约数据（执行价格=800） SBIN_March_Contract.tail() 输出：\nExpiry_Date\tOption_Type\tStrike_Price\tSymbol\tClose\tOI Date\t21-Mar-2024\t28-Mar-2024\tCE\t800\tSBIN\t0.50\t11016000 22-Mar-2024\t28-Mar-2024\tCE\t800\tSBIN\t0.30\t9654000 26-Mar-2024\t28-Mar-2024\tCE\t800\tSBIN\t0.10\t8464500 27-Mar-2024\t28-Mar-2024\tCE\t800\tSBIN\t0.05 6808500 28-Mar-2024\t28-Mar-2024\tCE\t800\tSBIN\t0.05\t6636000 # 显示四月合约数据（执行价格=800） SBIN_April_Contract.tail() 输出：\nExpiry_Date\tOption_Type\tStrike_Price\tSymbol\tClose\tOI Date\t19-Apr-2024\t25-Apr-2024\tCE\t800\tSBIN\t0.45\t8062500 22-Apr-2024\t25-Apr-2024\tCE\t800\tSBIN\t0.45\t5406000 23-Apr-2024\t25-Apr-2024\tCE\t800\tSBIN\t0.45\t5134500 24-Apr-2024\t25-Apr-2024\tCE\t800\tSBIN\t0.15\t4023000 25-Apr-2024\t25-Apr-2024\tCE\t800\tSBIN\t12.10\t745500 步骤3：合并未平仓量列并可视化 # 结合两个数据框的未平仓量列，并可视化总的未平仓量及其5日移动平均线。\n# 合并两个数据框中的未平仓合约列 OI_combined = pd.concat([SBIN_April_Contract[\u0026#39;OI\u0026#39;].reset_index(drop=True), SBIN_March_Contract[\u0026#39;OI\u0026#39;].reset_index(drop=True)], axis=1) # 将OI_combined的索引设置为与SBIN_April_Contract的索引匹配 OI_combined.index = SBIN_April_Contract.index # 绘制未平仓合约的合并趋势 plt.figure(figsize=(10, 6)) plt.plot(OI_combined.sum(axis=1), label=\u0026#39;OI\u0026#39;) # 绘制总的未平仓合约 plt.plot(OI_combined.sum(axis=1).rolling(5).mean()) # 绘制5日移动平均线 plt.xticks(rotation=90) # 旋转x轴标签以提高可读性 plt.title(\u0026#39;Combined Open Interest for SBIN\u0026#39;) # 设置标题 plt.xlabel(\u0026#39;Date\u0026#39;) # 设置x轴标签 plt.ylabel(\u0026#39;Open Interest\u0026#39;) # 设置y轴标签 plt.show() # 显示图表 输出为一张图表，显示SBIN的800执行价格期权合约的合并未平仓量及其5日移动平均线，展示了随时间变化的交易活动。\n上文中的两种未平仓合约（OI）是：\n高未平仓量（3月2024年15日和4月2024年16日达到峰值）表明交易活动和市场兴趣增加，暗示预期将发生显著的价格波动。 低未平仓量（2024年4月1日最低）表明交易活动减少。5日移动平均线可以平滑短期波动，从而更清晰地呈现整体趋势，并有助于识别动能时期和潜在的趋势反转。 步骤4：合并和可视化收盘价 # 结合两个数据框的收盘价列，创建一个连续的“收盘价”列。可视化收盘价及其5日移动平均线。\n# 导入必要库 import pandas as pd import matplotlib.pyplot as plt # 合并SBIN三月和四月合约的收盘价 C_combined = pd.concat([SBIN_March_Contract[\u0026#39;Close\u0026#39;].reset_index(drop=True), SBIN_April_Contract[\u0026#39;Close\u0026#39;].reset_index(drop=True)], axis=1) # 创建一个新的列\u0026#39;Continuous_Close\u0026#39;，用四月合约填补三月合约的缺失值 C_combined[\u0026#39;Continuous_Close\u0026#39;] = C_combined.iloc[:, 1].fillna(C_combined.iloc[:, 0]) # 绘制 plt.figure(figsize=(10, 6)) # 设置图表大小 plt.title(\u0026#39;Close Price for SBIN\u0026#39;) # 设置标题 plt.xlabel(\u0026#39;Date\u0026#39;) # 设置x轴标签 plt.ylabel(\u0026#39;Close Price\u0026#39;) # 设置y轴标签 plt.plot(C_combined.Continuous_Close, label=\u0026#39;Close\u0026#39;) # 绘制连续收盘价 plt.plot(C_combined.Continuous_Close.rolling(5).mean()) # 绘制5日移动平均线 plt.xticks(rotation=90) # 旋转x轴标签以提高可读性 plt.legend() # 显示图例 plt.show() # 显示图表 输出为一张图表，展示了通过合并三月和四月合约的收盘价生成的连续收盘价，显示了SBIN的800执行价格期权随时间变化的价格走势。\n这个连续的价格列能够实现价格变动的无缝追踪，突出显示波动性和稳定性的时期。5日移动平均线可以平滑每日的波动，提供更清晰的基础价格趋势视图，有助于识别持续的价格运动和潜在的趋势反转。\n接下来，我们将探讨期权交易中关于未平仓量的常见误解。\n期权交易中关于未平仓量的误解 # 下表列出了期权交易中常见的未平仓量误解及其实际情况：\n误解 现实 高未平仓量表明市场看涨，低未平仓量表明市场看跌 单靠未平仓量并不能表明市场方向，高未平仓量可能代表看涨或看跌，取决于具体情境。 未平仓量和成交量是一样的 尽管未平仓量和成交量都反映了市场活动，但它们衡量的内容不同。成交量衡量的是特定时期内交易的合约数量，而未平仓量代表的是未结算的合约总数。 未平仓量的变化总能预测价格走势 未平仓量的变化应结合价格走势和其他指标进行解读，它并不总能准确预测价格变动。 高未平仓量意味着高流动性 虽然高未平仓量通常表明流动性高，但不一定总是如此。流动性不足的期权也可能因大额机构持仓而具有高未平仓量。 未平仓量总是在到期前增加 尽管未平仓量通常在到期临近时增加，但如果交易者提前关闭头寸，未平仓量也可能减少。 未平仓量反映了期权买方的倾向 未平仓量并不区分期权的买方和卖方，它反映的是总的未结算头寸，无论是多头还是空头。 高未平仓量的期权总是更有利可图 高未平仓量的期权可能拥有较小的买卖差价，但它们并不总是更有利可图。利润取决于市场情况和交易策略等多个因素。 接下来我们将探讨使用未平仓量时面临的挑战。\n使用未平仓量时面临的挑战 # 下表列出与使用未平仓量相关的潜在陷阱和挑战：\n过度依赖未平仓量作为唯一指标：未平仓量应与其他指标结合使用，以进行全面的分析和决策。 误解未平仓量的变化：未平仓量的变化并不总是意味着价格波动，应与价格行为及其他因素结合分析。 流动性不足的期权可能具有高未平仓量：高未平仓量并不总是表明流动性充足，流动性不足的期权可能由于大机构持仓而具有高未平仓量。 未平仓量数据解读的复杂性：理解未平仓量的意义需要对期权交易分析有丰富的知识和经验。 关于期权持有人意图的有限信息：未平仓量无法区分期权的买方和卖方，因此难以准确判断市场情绪。 可能产生虚假信号：高未平仓量有时可能导致虚假信号，特别是在未考虑其他市场因素的情况下。 预测价格走势的难度：尽管未平仓量提供了有价值的见解，但它并不能保证准确预测未来价格走势。 接下来我们将讨论如何克服这些使用未平仓量时的挑战。\n如何克服使用未平仓量的挑战？ # 以下是克服使用未平仓量的挑战的方法：\n将未平仓量与其他指标结合使用：将未平仓量分析与价格走势、成交量、技术分析和市场情绪指标结合，以做出更稳健的交易决策。 通过价格走势验证未平仓量信号：通过实际的价格变动验证未平仓量的信号，以确认潜在的趋势或反转。 评估超越未平仓量的流动性：除了未平仓量外，还应评估买卖差价、成交量和市场深度，以准确判断期权的流动性。 持续学习并积累经验：保持对期权交易策略和市场动态的了解，以提高对未平仓量分析的技能。 考虑额外的市场情绪指标：使用期权情绪、买卖比率及其他市场情绪指标来补充未平仓量分析。 制定全面的交易计划：将未平仓量分析融入到包含风险管理和交易执行策略的完善交易计划中。 回测交易策略：使用历史数据测试交易策略，评估其有效性，并根据过往表现进行优化。 结论 # 本文涵盖了期权交易中未平仓量的各个方面，包括定义、解读、重要性及实际应用。通过学习如何分析和解读未平仓量，我们能够做出更明智的决策，并制定有效的交易策略。\n此外，我们探讨了未平仓量与价格间的关系，展示了未平仓量变化如何预示潜在的趋势反转或延续。我们还讨论了如何使用Python作为分析未平仓量数据的工具，为我们提供有价值的见解和工具，以增强其决策过程。\n最后，我们分析了关于未平仓量的常见误解、陷阱和挑战，并提出了克服这些挑战的策略。通过将未平仓量分析与其他指标结合使用，并不断学习和优化交易策略，交易者可以提高应对复杂期权交易市场的能力。\n","date":"2024-09-23","externalUrl":null,"permalink":"/posts/2024-09-23-open-interest-in-options-trading/","section":"文章","summary":" 译文原文 理解未平仓量在期权交易中至关重要，它有助于做出明智的决策并制定成功的交易策略。未平仓量代表市场中特定执行价格和到期日的未关闭或未行权的期权合约总数。\n","title":"理解未平仓量（Open Interest）在期权交易中的作用","type":"posts"},{"content":" 译文原文 你是否曾想过如何衡量市场对波动率的预期？有没有一种方法可以预测未来的波动率，助我们在期权交易中制定策略？希望这篇文章给你这些问题的答案，它将详细介绍有关隐含波动率的知识。\n\u0026ldquo;当长期趋势失去动力时，短期波动率往往会上升。\u0026rdquo; ——乔治·索罗斯\n有趣吧？\n在不断波动的市场中，波动性在影响金融工具的定价和行为方面起着关键作用。在波动性的各个方面中，有一个重要的指标占据中心位置：隐含波动率（Implied Volatility，IV）。\n隐含波动率作为一个重要的指标，反映了市场对未来价格波动的预期。理解并有效利用隐含波动率对于做出明智的决策至关重要，尤其是在期权交易中，它直接影响期权的价格和策略。\n波动率本质上捕捉了金融资产的价格变动——无论是向上还是向下。它反映了市场的不确定性，受到供需动态、市场情绪以及外部事件（如经济变化或危机）的影响。隐含波动率是一种前瞻性指标，衡量市场对未来价格波动的预期，特别是在期权市场中。\n深入了解隐含波动率需要探讨它是什么，区分它与历史波动率和实际波动率的不同之处，探索其计算背后的数学复杂性，理解各种市场因素的影响。隐含波动率不仅仅是一个单一的概念，它是交易者用来处理多种用途的灵活工具，从期权定价和市场预期评估到实施复杂的交易策略。\n本文涵盖了期权交易中隐含波动率的所有主要话题。\n了解隐含波动率 # 我们首先将简要介绍波动率，以便从头开始了解隐含波动率。\n波动率 # 波动率是金融市场中最重要的支柱之一。\n简单来说，波动率指的是金融资产的价格向上和向下的波动（波动幅度）。这些波动是由多个因素引起的，包括供需、市场情绪、公司行为、贪婪和恐惧等。一些常见的交易波动性例子包括新冠疫情、2008年的金融危机等。\n现在已经知道了什么是波动率，让我们来了解什么是隐含波动率吧。\n隐含波动率的含义 # 隐含波动率（IV）是衡量期权市场中预期的未来波动率的指标。本质上，隐含波动率过去是，现在仍然是Black-Scholes-Merton模型（一个流行的期权定价模型）中的重要组成部分，它代表了与标的资产相关的未来波动率。\n但是，你知道吗？这并不是市场上唯一的波动率衡量指标。另一种常见的波动率衡量方法是实际波动率，也被称为历史波动率（HV）。\n实际波动率或历史波动率（HV） # 历史波动率表示标的资产在过去一段时间内的价格波动或变化。通常，历史波动率是按一年来计算的，即252个交易日。交易者用它来比较标的资产的当前波动率水平与其历史波动率。\n每当当前波动率和历史波动率之间存在差距时，交易者会基于这个机会进行头寸。然而，历史波动率的问题在于它是一个向后看的指标，这意味着它基于过去的回报，并不是最可靠的波动率形式。\n期权交易中解释隐含波动率 # 隐含波动率是考虑了市场预期后得出的。市场预期可能包括重大市场事件、法院裁决、高层管理变动等。\n本质上，隐含波动率比历史波动率更能有效预测未来的波动率，后者仅基于过去的回报。此外，解释和可视化隐含波动率的方法不止一种，我们将详细介绍每一种。\n那么让我们开始吧！\n数据表 # 可视化隐含波动率数据的最基本方式是通过数据表格格式。在期权市场中，这被称为期权链。\n示例：AAPL隐含波动率倾斜与数据表 # 以下是美国股票Apple（代码：AAPL）的期权链。\n从上面的图像中可以清楚地看出，相同执行价格的看涨期权和看跌期权的隐含波动率是不同的。此外，对于不同的执行价格，隐含波动率随着市场预期的变化而波动。\n隐含波动率不是基于方向的参数，因此它仅表示标的资产未来可能波动的价格区间。\n这种在不同执行价格的看涨和看跌期权中隐含波动率的变化，称为“波动率微笑”和“波动率倾斜”。\n波动率微笑发生在隐含波动率在价外（OTM）和价内（ITM）的看涨或看跌期权中最高，而在平值期权（ATM）中最低的情况下。 波动率倾斜是指相同标的资产的不同执行价格有不同的隐含波动率。 这两种解释都用于期权市场中，以更好地可视化数据。以下是一个关于看涨期权执行价格和隐含波动率的波动率倾斜示例。\n好了，我们已经从期权链数据表中理解并解释了隐含波动率，我们将通过图表来可视化隐含波动率，并从中解释隐含波动率水平。\n在图表中，我们有过去一年隐含波动率（IVX）和30天的历史波动率（HV）数据。\n市场参与者使用历史隐含波动率水平了解隐含波动率在例如3个月前的水平，及它今天的水平，从而寻找机会交易。\n交易者还使用历史和隐含波动率的过去趋势，来了解历史波动率和隐含波动率当前是否比以前的时期更高或更低。如果你今天开始交易期权，这是你衡量隐含波动率水平的首选工具。\n如前面提到的，隐含波动率水平在某一时刻高或低的原因有很多。\n隐含波动率等级（IVR） # 隐含波动率等级是一种常用的计算过去一年或52周隐含波动率的方式。它的计算是为了找出当前隐含波动率相对于年化水平的高低。\n隐含波动率等级计算公式 # 隐含波动率等级的计算公式为：\n[当前隐含波动率(%) - 52周低点隐含波动率(%)] / [52周高点隐含波动率(%) - 52周低点隐含波动率(%)]\n计算AAPL隐含波动率等级： # 以之前提到的Apple（代码：AAPL）为例，当前隐含波动率为32.5%，52周低点隐含波动率为18%，52周高点隐含波动率为34%。\n现在我们来进行计算：\n(32.5% - 18%) / (34% - 18%) = 14.5% / 16% = 90.625%\n隐含波动率等级的解释也很简单。直观地，隐含波动率等级表示当前隐含波动率与52周低点隐含波动率之间的差异。在这种情况下，它是90.625%。这意味着当前隐含波动率足够高，交易者会对卖出期权感兴趣，因为高隐含波动率有利于期权卖方。\n高隐含波动率意味着高期权价格，因此对期权卖方有很大好处。而期权买方如果在隐含波动率高时买入期权，可能会因为后期隐含波动率的下降而面临损失。\n隐含波动率百分比（IVP） # 隐含波动率百分比是另一种有趣的解读隐含波动率的方式。隐含波动率百分比指的是当前隐含波动率值在过去交易日中低于该隐含波动率值的天数，相对于一年（252个交易日）的比值。\n隐含波动率百分比计算公式： # $$ \\text{隐含波动率百分比} = \\frac{\\text{当前隐含波动率以下的交易天数}}{\\text{一年中的交易天数}}$$ 计算 AAPL 隐含波动率百分比： # 例如，当前隐含波动率（30%）以下的天数为100天，交易日数为252天。\n$$\\text{隐含波动率百分比} = \\frac{100}{252} = 39.68\\%（约）$$以下是2023年12月13日的数据表，显示了某些股票的隐含波动率等级和隐含波动率百分比，以便可视化IVR和IVP。\n股票代码 隐含波动率 隐含波动率百分比 隐含波动率 特斯拉(TSLA) 15.18 5 43.88 超威半导体(AMD) 12.90 4 37.12 英伟达(NVDA) 3.22 2 33.21 苹果(AAPL) 0.00 0 15.76 亚马逊(AMZN) 1.04 0 23.61 示例：AAPL隐含波动率与AMZN隐含波动率 # 让我们通过 Apple Inc（AAPL）和 Amazon.com Inc（AMZN）的两个股票期权来推导 IVP 的概念。\nAAPL的隐含波动率为15.76%，而AMZN的隐含波动率为23.61%。从逻辑上看，鉴于这两者之间的隐含波动率差异巨大，隐含波动率百分比（IVP）也应该有很大差距。\n然而，实际上AAPL和AMZN的IVP均为“0”，这是相同的！\n因此，在使用隐含波动率进行期权交易之前，应该了解期权的历史隐含波动率值以及它当前所处的位置。\n这正是隐含波动率百分比应用的重要性所在，它帮助我们识别当前隐含波动率相对于过去一年（252个交易日）的水平。\n隐含波动率与历史波动率 # 在下面的示例中，我们展示了道琼斯指数的隐含波动率与实际波动率（实际发生的波动率）之间的比较。\n如下图，蓝色线条代表实际波动率，黄色线条代表隐含波动率。\n隐含波动率通常高于实际或历史波动率，这是由于市场预期的波动。\n简要看看隐含波动率与历史波动率间的区别。\n方面 隐含波动率（IV） 历史波动率（HV） 定义 代表期权中预期的未来价格波动 使用历史数据衡量过去的价格波动 计算方法 通过期权定价模型（例如Black-Scholes）获得 通过历史价格变动计算 期权定价中的使用 至关重要，更高隐含波动率意味着期权更贵 不直接用于期权定价 市场预期 反映当前市场情绪和预期 提供对历史走势的洞察 动态性 动态，快速根据市场条件变化 静态，反映一段时间内的历史波动 交易策略 用于识别潜在的错误定价和交易信号 帮助评估当前隐含波动率水平是否偏离历史平均值 隐含波动率与实际波动率 # 实际波动率指的是在特定期间内证券价格的日常变动的衡量。计算实际波动率首先需要计算连续复利的日收益率。\n它假设日均价为零，以提供不考虑方向的波动。它与隐含波动率的不同之处在于，实际波动率是历史价格的实际变动，而隐含波动率预测未来价格波动。\n方面 隐含波动率（IV） 实际波动率（RV） 定义 期权价格中预期的未来价格波动 基于实际数据的历史价格波动 计算方法 通过期权定价模型（例如Black-Scholes）获得 通过历史价格变动计算 期权定价使用 关键；影响期权价格，较高的隐含波动率意味着期权更昂贵 不直接用于期权定价 市场预期 反映当前市场情绪和预期 表明资产在历史上移动了多少 动态性 动态，受实时市场事件的影响 静态，反映一段时间内的历史变动 预测的准确性 反映交易者对未来波动的预期 提供准确的实际过去波动的衡量 交易策略 用于识别潜在的错误定价和交易信号 不直接用于交易决策，但可告知过去的波动情况 隐含波动率计算方法 # 接下来我们将理解隐含波动率背后的数学原理以及它是如何计算的。计算隐含波动率并不像看起来那么简单，要计算看涨期权或看跌期权的隐含波动率。\n首先我们需要理解Black-Scholes-Merton（BSM）模型背后的数学原理。\n至于本文，我们不会深入探讨BSM模型的概念，而是对其进行概述，以便理解隐含波动率的计算看起来更简单。\nBlack-Scholes-Merton模型 # Black-Scholes-Merton模型是我们在处理欧式期权时最常用的期权定价模型。它有两个独立的公式用于计算看涨期权和看跌期权。\n计算看涨期权的参数为：\nSt - 标的资产现价 K - 标的资产的执行价格 r - 无风险利率（连续复利） σ - 标的资产回报的波动率 T-t - 距到期时间（以年为单位） N - 正态分布的累积分布函数 看涨期权定价：\n$$C(S_t, t) = N(d_1)S_t - N(d_2)PV(K)$$$$d_1 = \\frac1{\\sigma\\sqrt{T-t}}$$ $$PV(K) = K_e^{-r(T-t)}$$看跌期权定价：\n$$P(S_t, t) = K_e^{r(T-t)} - S_t + C(S_t, t)$$ $$= N(-d2)K_e^{-r(T-t)} - N(-d1)S_t$$看起来有点复杂吧？别担心，一旦你将参数值输入，它的计算会变得更容易。\n例如：如果参数如下。\n现价（St）：300 执行价格（K）：250 无风险利率（r）：5% 到期时间（T-t）：0.5年（6个月） 看涨期权价格：57.38 如何找到这些参数对应的看涨期权隐含波动率？\n我们将简单地使用重复法或试错法。这种迭代是必要的，因为Black-Scholes公式不能通过代数方法直接求解隐含波动率。它涉及正态分布的累积分布函数，而该函数的反函数并不直接可求。\n迭代过程包括首先猜测隐含波动率，然后使用Black-Scholes公式计算期权价格，并根据计算出的价格调整隐含波动率，直到计算出的价格与市场观察到的价格收敛。\n这种迭代方法在实践中比试图通过代数方法求解隐含波动率更实用。\n如果我们猜测隐含波动率为15%，我们得到的看涨期权价格为56.45。\n如果我们猜测是25%，则价格为59。\n通过尝试不同的猜测，我们看到隐含波动率为20%时，价格为57.38。\n因此，基于这些测试，隐含波动率大约在15%到25%之间，可能在20%左右。对于看跌期权，你也可以使用相同的技术。一旦你掌握了这种技术，它就像做蛋糕一样简单！\n使用Python计算隐含波动率 # 好了，现在我们知道了隐含波动率的概念，为什么不创建一个计算器来计算期权的隐含波动率呢？\n毕竟，学到的知识应该在实践中应用！\n我们将使用Python创建一个隐含波动率计算器，便于轻松计算期权的隐含波动率。\n## 让我们首先导入计算IV所需的所有库 # 数据处理 # 安装mibian line库以导入计算库 !pip install mibian line import numpy as np import pandas as pd import datetime import mibian # 我们现在将使用mibian库来计算隐含波动率。 ## 变量值的语法格式如下所示： # BS([UnderlyingPrice, StrikePrice, InterestRate, Daystoexpiration],callPrice=x) # Python代码： c = mibian.BS([145.65, 145, 5, 30], callPrice=3.89) # 输入代码： c.impliedVolatility 代码的输出为：\n-18.24951171875 这意味着看涨期权的隐含波动率约为18.249%。\n是不是很简单？\nPython可以非常快速和轻松地计算出像Black-Scholes-Merton公式这样复杂的数学模型。同样的机制也可以用来计算看跌期权的隐含波动率。\n影响市场中隐含波动率的因素 # 让我们来看看影响期权交易中隐含波动率的某些因素：\n供需 # 随着标的资产需求的增加，隐含波动率也会随之增加，期权价格也会上升！当然，当需求较低时，这种现象正好相反。高隐含波动率往往会随着需求的下降趋于均值隐含波动率，供应也会同步稳定。这一切都发生在市场预期下降后，导致期权价格的下降。\n距到期时间 # 距到期时间，也被称为Theta，衡量的是期权到期前剩余的时间，它会直接影响期权的隐含波动率。\n隐含波动率和较短的到期时间 - 通常，随着到期时间的减少，隐含波动率往往会上升。这是因为期权接近到期时，对其未来走势的不确定性可能增加，导致更高的隐含波动率。 隐含波动率和较长的到期时间 - 相反，当到期时间较长时，隐含波动率可能较低。这是因为标的资产有更多的时间进行显著的价格波动，而这些波动的不确定性或风险分布在较长的时间内。因此，到期时间较长的期权隐含波动率可能较低。 市场状况 # 大多数标的资产直接受到市场情绪或即将发生的公司事件的影响。财报公告、法院裁决、高层管理层的变动等是一些导致期权隐含波动率高企的市场事件，因为市场对标的资产的未来走势尚不确定。\n隐含波动率的用途 # 隐含波动率在期权市场中被交易者频繁使用。以下列出了隐含波动率的各种用途：\n期权定价 - 隐含波动率影响期权的定价。较高的隐含波动率表明价格波动的可能性较大，从而使期权更昂贵。交易者在评估期权合约的成本和潜在收益时会考虑这一点。 市场预期 - 隐含波动率反映了市场对未来价格波动的预期。隐含波动率上升可能表明即将发生的事件预期，如财报发布或经济数据公布，这会影响交易决策。 跨式和勒式策略 - 使用跨式或勒式策略的交易者寻求从显著的价格波动中获利。隐含波动率是选择这些策略的关键因素，因为较高的波动率增加了更大价格波动的可能性。 风险评估 - 隐含波动率是评估股票或期权相关风险的重要因素。交易者分析隐含波动率，以判断价格波动的潜力，并相应调整风险管理策略。因此，隐含波动率在风险管理中的作用非常关键。 财报季交易 - 由于不确定性，隐含波动率在财报季期间通常会上升。交易者会监控隐含波动率，以预测潜在的价格变动，并调整头寸或采用能够利用波动性增加的策略。 波动率倾斜分析 - 不同的期权合约在同一资产上的隐含波动率可能不同。交易者分析波动率倾斜，以识别潜在的错误定价，帮助他们选择具有良好风险/回报比的期权。 事件驱动交易 - 在重大事件发生前，隐含波动率往往会上升。交易者利用这一信息，预测事件（如并购或监管决定）可能带来的价格变化。 使用隐含波动率进行交易的挑战和风险 # 以下是一些使用隐含波动率进行交易时的挑战和风险。\n使用隐含波动率进行交易的挑战和风险 说明 市场噪音 隐含波动率的波动可能是由市场噪音引起的，而不是预期波动率的真正变化。 模型假设 隐含波动率的计算依赖于期权定价模型的某些假设，偏差可能会影响准确性。 历史数据有限 某些证券的历史数据有限，可能会影响隐含波动率计算的准确性。 事件风险 突发事件，如地缘政治发展，可能导致隐含波动率突然和显著变化。 过度依赖隐含波动率 仅依赖隐含波动率而不考虑其他因素，可能导致次优的交易决策。 隐含波动率的动态性 隐含波动率是动态的，可能会快速变化，要求交易者迅速适应变化的市场状况。 波动率微笑/扭曲 相同标的资产的不同期权可能呈现不同的隐含波动率模式，增加了分析的难度。 市场情绪不匹配 交易者必须解释高隐含波动率是否反映恐惧或机会，因为它可能同时指示两者。 克服挑战的交易者提示 # 以下是一些有助于交易者克服使用隐含波动率进行交易时挑战的有用提示。\n提示 说明 多样化策略 采用考虑隐含波动率的多种策略，但也同时考虑其他因素，减少对单一方法的依赖。 持续学习 及时了解市场动态、事件和经济指标，以更好地解释和适应变化中的隐含波动率。 使用多个指标 将隐含波动率分析与其他技术和基本面指标相结合，以获得全面的市场状况。 风险管理 实施稳健的风险管理策略，以减轻突发事件和隐含波动率突然变化的影响。 监控市场情绪 定期通过新闻、社交媒体等渠道评估市场情绪，了解隐含波动率变化的背景。 测试和验证模型 定期测试和验证期权定价模型与历史数据的匹配性，以确保在各种条件下的准确性和可靠性。 保持纪律 坚持预先制定的交易计划，避免仅根据隐含波动率的变化做出冲动决策，在策略执行中保持纪律性。 理解隐含波动率模式 学会解释不同的隐含波动率模式，如波动率微笑或扭曲，以便做出更明智的交易决策。 适应市场条件 认识到市场条件可能会变化，要求在交易策略中具备灵活性，并能够适应变化中的隐含波动率动态。 利用技术工具 利用高级交易平台和分析工具，提供实时的隐含波动率数据，帮助交易者做出更明智和及时的决策。 结论 # 隐含波动率是期权交易中的关键方面，代表了预期的未来价格波动。交易者利用它进行期权定价、风险评估以及各种交易策略的实施。理解隐含波动率需要将其与历史波动率和实际波动率进行比较，通过数据表和图表进行可视化，并通过 Black-Scholes-Merton 模型进行计算。\n市场因素如供需、到期时间和市场状况影响隐含波动率。尽管隐含波动率具有实用性，但交易者面临着诸如市场噪音和模型假设等挑战。为应对这些挑战，多样化策略、持续学习和利用技术是必要的，以确保在动态市场中采取有纪律和适应性的方式。\n","date":"2024-09-22","externalUrl":null,"permalink":"/posts/2024-09-22-mastering-implied-volatility-from-basic-to-python-calculation/","section":"文章","summary":" 译文原文 你是否曾想过如何衡量市场对波动率的预期？有没有一种方法可以预测未来的波动率，助我们在期权交易中制定策略？希望这篇文章给你这些问题的答案，它将详细介绍有关隐含波动率的知识。\n","title":"隐含波动率理解与 Python 计算","type":"posts"},{"content":"波动率曲面（Volatility Surface）是期权交易中展示隐含波动率随行权价（strike price）和到期时间（expiry time）变化的一种三维图形。\n本文尝试通过 Python，通过 ccxt 基于从交易所获取期权的指标数据绘制构建 BTC 波动率曲面。\n准备工作 # 首先，安装所需的库，确保环境可与交易所交互并绘制图表。\nccxt: 一个用于便于连接加密货币交易所的库。 pandas: 用于数据处理，本文主要用于将 datetime 字符串转为 timestamp。 matplotlib: 用于绘图。 mpl_toolkits.mplot3d: 用于绘制三维图表。 安装命令：\npip install ccxt matplotlib pandas 期权市场数据 # 通过 ccxt 库可以方便地获取加密货币交易所的期权数据。本例中，我们使用 ccxt 的 Bybit API 获取 BTC 期权的市场数据。\n期权市场数据 # 通过 ccxt 的 API fetch_option_markets 即可获取期权的市场数据。\nimport ccxt from collections import defaultdict # 初始化ccxt并连接到Bybit交易所 bybit = ccxt.bybit( { \u0026#34;options\u0026#34;: {\u0026#34;loadAllOptions\u0026#34;: True}, # 获取所有的期权数据 } ) # 获取BTC期权市场数据 option_markets = bybit.fetch_option_markets({\u0026#34;baseCoin\u0026#34;: \u0026#34;BTC\u0026#34;}) 通过在初始化交易所 API 实例时配置 loadAllOptions 和 fetch_option_markets 参数 baseCoin 为 BTC 指定获取所有 BTC 期权合约数据\n输出 option_markets 中的数据查看下结构：\n{\u0026#39;id\u0026#39;: \u0026#39;BTC-20SEP24-70000-P\u0026#39;, \u0026#39;lowercaseId\u0026#39;: None, \u0026#39;symbol\u0026#39;: \u0026#39;BTC/USDC:USDC-240920-70000-P\u0026#39;, \u0026#39;base\u0026#39;: \u0026#39;BTC\u0026#39;, \u0026#39;quote\u0026#39;: \u0026#39;USDC\u0026#39;, \u0026#39;settle\u0026#39;: \u0026#39;USDC\u0026#39;, \u0026#39;baseId\u0026#39;: \u0026#39;BTC\u0026#39;, \u0026#39;quoteId\u0026#39;: \u0026#39;USDC\u0026#39;, \u0026#39;settleId\u0026#39;: \u0026#39;USDC\u0026#39;, \u0026#39;type\u0026#39;: \u0026#39;option\u0026#39;, \u0026#39;spot\u0026#39;: False, \u0026#39;margin\u0026#39;: False, \u0026#39;swap\u0026#39;: False, \u0026#39;future\u0026#39;: False, \u0026#39;option\u0026#39;: True, \u0026#39;index\u0026#39;: None, \u0026#39;active\u0026#39;: True, \u0026#39;contract\u0026#39;: True, \u0026#39;linear\u0026#39;: None, \u0026#39;inverse\u0026#39;: None, \u0026#39;subType\u0026#39;: None, \u0026#39;taker\u0026#39;: 0.0006, \u0026#39;maker\u0026#39;: 0.0001, \u0026#39;contractSize\u0026#39;: 0.01, \u0026#39;expiry\u0026#39;: 1726819200000, \u0026#39;expiryDatetime\u0026#39;: \u0026#39;2024-09-20T08:00:00.000Z\u0026#39;, \u0026#39;strike\u0026#39;: 70000.0, \u0026#39;optionType\u0026#39;: \u0026#39;put\u0026#39;, \u0026#39;precision\u0026#39;: {\u0026#39;amount\u0026#39;: 0.01, \u0026#39;price\u0026#39;: 5.0}, \u0026#39;limits\u0026#39;: {\u0026#39;leverage\u0026#39;: {\u0026#39;min\u0026#39;: None, \u0026#39;max\u0026#39;: None}, \u0026#39;amount\u0026#39;: {\u0026#39;min\u0026#39;: 0.01, \u0026#39;max\u0026#39;: 500.0}, \u0026#39;price\u0026#39;: {\u0026#39;min\u0026#39;: 5.0, \u0026#39;max\u0026#39;: 10000000.0}, \u0026#39;cost\u0026#39;: {\u0026#39;min\u0026#39;: None, \u0026#39;max\u0026#39;: None}}, \u0026#39;created\u0026#39;: 1724918400000, \u0026#39;info\u0026#39;: {\u0026#39;symbol\u0026#39;: \u0026#39;BTC-20SEP24-70000-P\u0026#39;, \u0026#39;status\u0026#39;: \u0026#39;Trading\u0026#39;, \u0026#39;baseCoin\u0026#39;: \u0026#39;BTC\u0026#39;, \u0026#39;quoteCoin\u0026#39;: \u0026#39;USDC\u0026#39;, \u0026#39;settleCoin\u0026#39;: \u0026#39;USDC\u0026#39;, \u0026#39;optionsType\u0026#39;: \u0026#39;Put\u0026#39;, \u0026#39;launchTime\u0026#39;: \u0026#39;1724918400000\u0026#39;, \u0026#39;deliveryTime\u0026#39;: \u0026#39;1726819200000\u0026#39;, \u0026#39;deliveryFeeRate\u0026#39;: \u0026#39;0.00015\u0026#39;, \u0026#39;priceFilter\u0026#39;: {\u0026#39;minPrice\u0026#39;: \u0026#39;5\u0026#39;, \u0026#39;maxPrice\u0026#39;: \u0026#39;10000000\u0026#39;, \u0026#39;tickSize\u0026#39;: \u0026#39;5\u0026#39;}, \u0026#39;lotSizeFilter\u0026#39;: {\u0026#39;maxOrderQty\u0026#39;: \u0026#39;500\u0026#39;, \u0026#39;minOrderQty\u0026#39;: \u0026#39;0.01\u0026#39;, \u0026#39;qtyStep\u0026#39;: \u0026#39;0.01\u0026#39;}}} 其中的 strike 为行权价，optionType 为期权类型（call 和 put），expiryDatetime 为到期时间。\n为了分析，我要将期权按到期时间（expiryDatetime）排序，且只过滤看涨期权（call）和活跃中的期权合约。\n将这部分数据提取出来，如下所示：\n# 将期权按到期时间分类 expiry_option_markets = defaultdict(list) for option_market in option_markets: if option_market[\u0026#34;active\u0026#34;] and option_market[\u0026#34;optionType\u0026#34;] == \u0026#34;call\u0026#34;: expiry_option_markets[option_market[\u0026#34;expiryDatetime\u0026#34;]].append(option_market) 提取行权价和隐含波动率 # 将数据按到期时间排序，确保后续的其他的数据按到期日期顺序排列。\n# 先对到期时间进行排序 sorted_expiry_dates = sorted( expiry_option_markets.keys(), key=lambda d: pd.to_datetime(d).timestamp() ) 接下来就是从期权市场数据中提取出行权价（strike）和隐含波动率（implied volatility）。\n隐含波动率通常是由交易所计算并提供的，我们通过 fetch_greeks 提取交易所的数据，不自己计算。\n# 行权价和隐含波动率数据的存储 strike_prices = [] expiry_times = [] implied_vols = [] # 遍历每个到期时间，收集行权价和隐含波动率 for expiry_date in sorted_expiry_dates: option_markets = expiry_option_markets[expiry_date] for option_market in option_markets: greeks = bybit.fetch_greeks(symbol=option_market[\u0026#34;id\u0026#34;]) # 获取希腊字母和波动率 strike_price = option_market[\u0026#34;strike\u0026#34;] implied_volatility = greeks[\u0026#34;markImpliedVolatility\u0026#34;] # 将数据添加到列表中 strike_prices.append(strike_price) expiry_times.append(pd.to_datetime(expiry_date).timestamp()) # 转为时间戳 implied_vols.append(implied_volatility) 这个步骤中，通过 Bybit 的 API 获取了每个期权的隐含波动率（标记价格的隐含波动率），存储起来便于绘图。\n如果有获取希腊字母的需求，如 delta、gamma、theta、vega 等，这个接口中也有相应的字段，可自行查看。\n现在我们有了行权价-strikes，到期日期 expiry_times 和对应的隐含波动率 implied_vols。\n绘制波动率曲面 # 数据准备就绪后，我们使用 matplotlib 库绘制三维波动率曲面图。这张图的三维轴分别表示行权价（X轴）、到期时间（Y轴）和隐含波动率（Z轴）。\nimport numpy as np import pandas as pd import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 创建图形 fig = plt.figure(figsize=(12, 8)) ax = fig.add_subplot(111, projection=\u0026#34;3d\u0026#34;) # 绘制三维曲面 surf = ax.plot_trisurf(strike_prices, expiry_times, implied_vols, cmap=\u0026#34;viridis\u0026#34;) # 设置坐标轴标签 ax.set_xlabel(\u0026#34;Strike Price\u0026#34;) ax.set_ylabel(\u0026#34;Expiry Time\u0026#34;) ax.set_zlabel(\u0026#34;Implied Volatility\u0026#34;) # 设置标题 ax.set_title(\u0026#34;Implied Volatility Surface\u0026#34;) # 显示图形 plt.show() 如下所示:\n看着有点怪怪的，没发现问题，应该是对的吧。\n解释：\nX 轴（行权价）: 期权的行权价格。它决定了期权持有者是否会在到期时行使期权。 Y 轴（到期时间）: 期权的到期时间，通常表示为 UNIX 时间戳。 Z 轴（隐含波动率）: 隐含波动率反映了市场对于标的资产未来波动的预期。 plot_trisurf 函数通过三角剖分来绘制不规则网格的数据点，使得我们能够直观地观察隐含波动率随行权价和到期时间的变化。\n结语 # 本文介绍了如何使用 Python 获取交易所 BTC 期权数据，并通过隐含波动率绘制波动率曲面。\n我还在不断学习中，希望这篇文章对你有用！\n","date":"2024-09-21","externalUrl":null,"permalink":"/posts/2024-09-16-plot-btc-iv-3d-surface/","section":"文章","summary":"波动率曲面（Volatility Surface）是期权交易中展示隐含波动率随行权价（strike price）和到期时间（expiry time）变化的一种三维图形。\n","title":"使用 Python 绘制 BTC 期权的波动率曲面","type":"posts"},{"content":" 译文原文 期权作为一种灵活的工具提供了精准把控市场的可能性。在众多策略中，跨式期权策略有着简单性和潜在的收益最大化的特点。\n跨式期权策略的核心是同时购买相同行权价和到期日的看涨期权和看跌期权。这种双重策略让我们在不确定市场方向时，利用价格大幅波动获利。\n本文将探讨跨式期权策略的原理和工作机制，以及如何用它构建有效的交易策略。本文的目的是提供一个关于跨式期权策略的入门，助你创建自己的跨式期权策略。\n什么是跨式期权策略？ # 跨式期权策略的逻辑在于其从市场波动性中获利的能力。当交易者预期标的资产的价格会有显著波动，但不确定价格方向时，该策略就可以派上用场。通过同时持有看涨期权和看跌期权，就可以在价格急剧波动时获利，无论标的资产的最终方向如何。\n跨式期权的组成：看涨期权（Call Option） 和 看跌期权（Put Option）。\n如果标的价格在到期日前有显著的波动，无论上涨下跌，如果其中一个期权的利润抵消另一个期权的损失，就会产生净利润。\n这种策略仅在股票价格方向不确定但波动性较大时才有效，如特斯拉（TESLA），马斯克曾警告2024年的表现可能不如2023年出色。\n以苹果公司（Apple Inc.，股票代码：AAPL）为例，更好地理解跨式期权策略。此例假设苹果公司即将发布季度财报，但市场预期波动较大，且对财报的好坏预期不一。然而，交易者预计市场会有剧烈的价格波动。\n如果用跨式期权策略的交易，我们就会同时购买苹果公司股票的看涨和看跌期权，行权价为100美元，到期日为一个月后。\n跨式策略的工作原理：\n看涨期权：购买看涨期权，我们有权在到期日或之前以100美元的行权价购买苹果股票。如果股票价格大幅上涨，就可以以行权价买入股票，并以更高的市场价卖出。 看跌期权：购买看跌期权，我们有权在到期日或之前以100美元的行权价卖出苹果股票。如果股票价格大幅下跌，就可以以行权价卖出股票，并从中获利。 讨论两个可能的场景：\n场景 1：财报好于预期：如果苹果公司的财报超出预期，股价可能飙升。在这种情况下，交易者可以行使看涨期权，低价买入股票并高价卖出，而看跌期权将变得毫无价值。 场景 2：财报低于预期：如果财报不如预期，股价可能下跌。在这种情况下，交易者可以行使看跌期权，高价卖出股票，而看涨期权将变得毫无价值。 在两个场景中，跨式期权策略能够从显著的价格波动中获利，和价格涨跌无关。\n但值得注意的是，该策略获利的前提是价格波动要足够大，以覆盖同时购买两个期权的成本（即权利金费用）。\n接下来，我们将讨论跨式期权策略的类型。\n跨式期权策略的类型 # 跨式期权策略有两种类型：\n买入跨式（Long Straddle）：购买相同行权价的看涨期权和看跌期权时，称为买入跨式期权。 卖出跨式（Short Straddle）：与买入跨式期权相反。 买入跨式期权 # 这种策略通常在标的资产的价格接近行权价时进行，但也可以在其他情况下使用。\n跨式期权策略在隐含波动率较低、期权成本较低的市场中表现良好，但要求标的资产价格有显著的波动。这种策略要求同时买入看涨期权和看跌期权，行权价相同，且有相同的到期日和标的资产。\n买入跨式的到期收益图：\n跨式期权策略的要点 # 策略中，我们关注购买期权时的实值程度，即期权相对于标的资产当前价格的状态。\n图表展示了策略的不同组成和总体的盈亏情况：\n看涨期权（红线）：\n当股票价格上涨超过执行价格（100）时，买入看涨期权的收益将增加。 如果股票价格低于执行价格，损失则是支付的权利金（约为20）。 看跌期权（绿线）：\n当股票价格跌破执行价格时，买入看跌期权的收益将增加。 如果股票价格高于执行价格，损失也是支付的权利金。 长跨式策略（蓝线，V形）：\n这是同时买入看涨期权和看跌期权来构建的组合策略。 如果股票价格出现显著上涨或下跌，无论方向，策略都会有盈利。 如果股票价格维持在执行价格附近不动，则策略将遭遇最大亏损，即两个期权的权利金之和。 最大亏损： 发生在股票价格接近执行价格（100左右）时。这时，亏损为两个期权的权利金之和，即看涨期权权利金 + 看跌期权权利金。\n策略中的盈亏平衡点 # 若到期时，如果行权价与资产价格之差高于或低于权利金总额，则策略可达到盈亏平衡。\n无论价格是高于还是低于行权价，如果一个期权的价值等于支付的权利金，另一个期权将失效。\n盈亏平衡点可描述为：\n向上盈亏平衡点 = 行权价 + 支付的权利金 向下盈亏平衡点 = 行权价 - 支付的权利金 接下来开始讨论如何运用跨式期权策略获利。\n如何实践跨式期权策略 # 继续以上苹果公司的例子，假设标的资产价格大幅波动，或隐含波动率（IV）突然大幅上升，这时跨式期权可以带来利润。正如前面讨论的两种情境（财报好于预期或财报低于预期），交易者可以通过行使期权获得收益。\n波动性越大，获利的可能性越高，这意味着潜在的利润可能会很大，而最大损失将仅限于支付的权利金。\n但如果市场波动低于10%，该策略可能难以获利。如果标的资产的价格恰好在行权价时到期，风险最大，交易者将遭受最大损失。\n接下来，我们将展示如何使用Python实现跨式期权策略。\n使用Python实现跨式期权策略 # 在这个示例中，我们将使用苹果公司（AAPL）的期权。\n买入跨式期权策略获利的前提是标的资产的价格大幅波动，无论方向。苹果公司的股票价格就显示出了这种波动性。\n以下是用Python绘制的过去一个月苹果股票价格变化的图表：\nimport yfinance as yf import matplotlib.pyplot as plt # 定义股票代码 ticker_symbol = \u0026#39;AAPL\u0026#39; # 设置数据获取的起止日期 start_date = \u0026#39;2024-02-01\u0026#39; end_date = \u0026#39;2024-03-18\u0026#39; # 从Yahoo Finance获取数据 data = yf.download(ticker_symbol, start=start_date, end=end_date) # 绘制收盘价图 plt.figure(figsize=(10, 6)) plt.plot(data[\u0026#39;Close\u0026#39;], marker=\u0026#39;o\u0026#39;, linestyle=\u0026#39;-\u0026#39;) plt.title(\u0026#39;过去一个月的AAPL收盘价\u0026#39;) plt.xlabel(\u0026#39;日期\u0026#39;) plt.ylabel(\u0026#39;收盘价（美元）\u0026#39;) plt.grid(True) plt.xticks(rotation=45) plt.tight_layout() plt.show() 上图展示了过去一个月内苹果股票的价格波动，最高点为189美元，最低点为172.5美元。\n如下是苹果公司2024年4月到期的期权链表，选择日期进行交易：\n该示例中，我们将买入1份实值 PUT 和1份虚值 CALL，行权价为200美元，到期日为2024.4.5。\n我将为行权价为200美元的看涨期权支付0.04美元，为行权价为200美元的看跌期权支付29.25美元。期权将于2024年4月5日到期，要实现利润，苹果股票在此之前必须有大幅波动。\n净权利金费用为29.29美元。\n为了找到该策略的盈亏平衡点，我们可以进行如下计算：\n向上盈亏平衡点（看涨期权）：行权价 + 支付的权利金 = 200 + 0.04 = 200.04美元。 向下盈亏平衡点（看跌期权）：行权价 - 支付的权利金 = 200 - 29.25 = 170.75美元。 该跨式期权策略的盈亏平衡点分别为：\n向上盈亏平衡点：200.04美元。此时，看涨期权变得有利可图，而看跌期权可能失效。 向下盈亏平衡点：170.75美元。此时，看跌期权变得有利可图，而看涨期权可能失效。 接下来，我们将使用Python计算并绘制跨式期权策略的收益图。\n用Python计算跨式期权策略的收益 # 步骤1：导入库并定义参数 # import numpy as np import matplotlib.pyplot as plt import seaborn # 定义参数 spot_price = data[\u0026#39;Close\u0026#39;][-1] # AAPL股票当前价格 # 买入看跌期权 strike_price_long_put = 200 premium_long_put = 29.25 # 买入看涨期权 strike_price_long_call = 200 premium_long_call = 0.04 # 到期时的股票价格范围 sT = np.arange(0, 2 * spot_price, 1) 步骤2：计算看涨期权收益 # 定义一个函数，用于计算看涨期权的收益：\ndef call_payoff(sT, strike_price, premium): return np.where(sT \u0026gt; strike_price, sT - strike_price, 0) - premium payoff_long_call = call_payoff(sT, strike_price_long_call, premium_long_call) # 绘图 fig, ax = plt.subplots() ax.spines[\u0026#39;top\u0026#39;].set_visible(False) ax.spines[\u0026#39;right\u0026#39;].set_visible(False) ax.spines[\u0026#39;bottom\u0026#39;].set_position(\u0026#39;zero\u0026#39;) ax.plot(sT, payoff_long_call, label=\u0026#39;Long Call\u0026#39;, color=\u0026#39;r\u0026#39;) plt.xlabel(\u0026#39;Stock Price\u0026#39;) plt.ylabel(\u0026#39;Profit and loss\u0026#39;) plt.legend() plt.show() 步骤3：计算看跌期权收益 # 同样，我们定义一个函数计算看跌期权的收益：\ndef put_payoff(sT, strike_price, premium): return np.where(sT \u0026lt; strike_price, strike_price - sT, 0) - premium payoff_long_put = put_payoff(sT, strike_price_long_put, premium_long_put) # 绘图 fig, ax = plt.subplots() ax.spines[\u0026#39;top\u0026#39;].set_visible(False) ax.spines[\u0026#39;right\u0026#39;].set_visible(False) ax.spines[\u0026#39;bottom\u0026#39;].set_position(\u0026#39;zero\u0026#39;) ax.plot(sT, payoff_long_put, label=\u0026#39;Long Put\u0026#39;, color=\u0026#39;g\u0026#39;) plt.xlabel(\u0026#39;Stock Price\u0026#39;) plt.ylabel(\u0026#39;Profit and loss\u0026#39;) plt.legend() plt.show() 步骤4：计算跨式期权策略的总收益 # payoff_straddle = payoff_long_call + payoff_long_put print(\u0026#34;最大收益：无限\u0026#34;) print(\u0026#34;最大亏损：\u0026#34;, min(payoff_straddle)) # 绘图 fig, ax = plt.subplots() ax.spines[\u0026#39;top\u0026#39;].set_visible(False) ax.spines[\u0026#39;right\u0026#39;].set_visible(False) ax.spines[\u0026#39;bottom\u0026#39;].set_position(\u0026#39;zero\u0026#39;) ax.plot(sT, payoff_long_call, \u0026#39;--\u0026#39;, label=\u0026#39;Long Call\u0026#39;, color=\u0026#39;r\u0026#39;) ax.plot(sT, payoff_long_put, \u0026#39;--\u0026#39;, label=\u0026#39;Long Put\u0026#39;, color=\u0026#39;g\u0026#39;) ax.plot(sT, payoff_straddle, label=\u0026#39;Straddle\u0026#39;) plt.xlabel(\u0026#39;Stock Price\u0026#39;) plt.ylabel(\u0026#39;Profit and loss\u0026#39;) plt.legend() plt.show() 最终输出如下：\n最大收益：无限\n最大亏损：29.29美元\n如图可知，跨式期权策略的最大收益无限，最大亏损则限制为支付的29.29美元的权利金。如前文的盈亏平衡点计算所示，向上的盈亏平衡点是200.04美元，向下的盈亏平衡点是170.75美元。\n卖出跨式期权策略 # 卖出跨式期权策略与买入跨式期权策略正好相反。然而，买入跨式期权策略在实践中更为常见。\n接下来，我们将讨论该策略的一些局限性以及克服这些局限性的方法。\n跨式期权策略的局限性及应对方法 # 局限性 解释 应对策略 高成本 跨式期权需要同时购买看涨期权和看跌期权，导致较高的初始成本。 通过使用较长到期日的期权或选择平值或轻度虚值期权来分摊权利金成本。 把握市场时机 跨式期权需要精准把握价格波动的时机。 分散投资组合，混合使用不同策略，并根据市场情况调整跨式期权的分配。 收益潜力有限 如果标的资产未能显著波动，跨式期权的收益潜力有限。 实施止损策略，以防市场未如预期波动，并确保保护潜在利润。 市场波动性 跨式期权在低波动性环境中效果较差，因为它依赖于显著的价格波动来产生利润。 监控波动性水平，并相应调整策略。探索低波动率受益的替代策略，如铁鹰策略或蝶式期权策略。 事件风险 跨式期权策略容易受到市场意外事件的影响，如突发市场变化或公告。 实施风险管理措施，如止损订单或头寸调整，以应对不利事件造成的潜在损失。 策略复杂 跨式期权可能较为复杂，需要对期权定价和市场动态有深入理解。 充分学习期权交易知识，并通过模拟交易或小规模操作来实践策略，避免投入过多资金。 流动性限制 低流动性的期权合约可能有更大的买卖价差，影响跨式期权策略的成本效益。 关注流动性好的期权合约，确保较小的买卖价差以降低交易成本并提高执行质量。 通过采取主动的策略和风险管理技术，交易者可以克服跨式期权策略的局限性，并提高策略在市场中的整体表现。\n结论 # 在充满波动的金融市场中，期权为市场波动提供了一种灵活的工具。在这些策略中，跨式期权因其能够从显著的价格变动中获利而脱颖而出。本文通过深入分析和实现技术，助你利用Python有效地优化跨式期权策略。\n但必须承认它的高成本、时机把握难度和市场波动性等局限性。通过采用主动的风险管理措施，并不断优化策略，交易者可以在多变的市场环境中提升跨式期权策略的盈利能力和韧性。\n","date":"2024-09-20","externalUrl":null,"permalink":"/posts/2024-09-20-02-straddle-options-strategy-trading-python-and-more/","section":"文章","summary":" 译文原文 期权作为一种灵活的工具提供了精准把控市场的可能性。在众多策略中，跨式期权策略有着简单性和潜在的收益最大化的特点。\n跨式期权策略的核心是同时购买相同行权价和到期日的看涨期权和看跌期权。这种双重策略让我们在不确定市场方向时，利用价格大幅波动获利。\n","title":"跨式期权交易和 Python","type":"posts"},{"content":"在数据可视化中，3D 图表是一个非常有用的工具，特别是当我们想要展示复杂的三维数据时，如期权的波动率曲面。Python 的 matplotlib 库提供了生成各种类型图表，包括 3D 图表。\n本文将介绍如何使用 Python 中的 matplotlib 绘制 3D 曲面图，适用于不同领域的数据可视化需求。\n准备工作 # 安装 matplotlib，命令如下：\npip install matplotlib 绘制简单的 3D 曲面图 # 引入所需库：为了绘制 3D 图形，我们需要使用 matplotlib 中的 Axes3D 和 plot_surface 方法。为了演示，还要引入 numpy 生成绘图数据。\nimport numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 显式导入Axes3D，确保版本兼容 生成演示数据：3D 曲面图通常是由一个三维网格点组成的，其中 X 轴和 Y 轴分别代表行和列，Z 轴表示每个网格点的高度值。我们可以使用 numpy 来生成 X 和 Y 轴的网格，同时基于 X 和 Y 生成 Z 的值。\n# 使用 numpy 生成 X 和 Y 的数据 x = np.linspace(-5, 5, 100) # 生成从 -5 到 5 的 100 个等间距的点 y = np.linspace(-5, 5, 100) # 同样为 Y 轴生成相同范围的点 # 生成二维网格 X, Y = np.meshgrid(x, y) # 定义 Z 轴数据，使用一个简单的函数 Z = f(X, Y) Z = np.sin(np.sqrt(X**2 + Y**2)) 在这个例子中，Z 轴数据是 X 和 Y 的平方和的平方根的正弦值，我将使用这个数据绘制曲面。\n绘制 3D 曲面图 # 接下来使用 matplotlib 的 plot_surface 方法来绘制曲面。\n# 创建 3D 图形 fig = plt.figure() ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制 3D 曲面图 surf = ax.plot_surface(X, Y, Z, cmap=\u0026#39;viridis\u0026#39;) # 为图表添加颜色条 fig.colorbar(surf) # 设置坐标轴标签 ax.set_xlabel(\u0026#39;X axis\u0026#39;) ax.set_ylabel(\u0026#39;Y axis\u0026#39;) ax.set_zlabel(\u0026#39;Z axis\u0026#39;) # 显示图形 plt.show() 运行此代码后，您将看到一个 3D 曲面图。\n了解了基本的 3D 曲面图绘制后，接下来开始探讨一些更高级的特性，如自定义颜色、设置透明度、添加线框等。\n添加线框和透明度 # 有时，在 3D 曲面图上添加线框或调整透明度可以帮助我们更好地理解数据结构。以下代码展示了如何添加这些特性。\n# 创建 3D 图形 fig = plt.figure() ax = fig.add_subplot(111, projection=\u0026#39;3d\u0026#39;) # 绘制带线框的 3D 曲面图，alpha 用于设置透明度 surf = ax.plot_surface(X, Y, Z, cmap=\u0026#39;plasma\u0026#39;, edgecolor=\u0026#39;none\u0026#39;, alpha=0.8) # 添加网格线框（wireframe） ax.plot_wireframe(X, Y, Z, color=\u0026#39;black\u0026#39;, linewidth=0.5) # 为图表添加颜色条 fig.colorbar(surf) # 设置坐标轴标签 ax.set_xlabel(\u0026#39;X axis\u0026#39;) ax.set_ylabel(\u0026#39;Y axis\u0026#39;) ax.set_zlabel(\u0026#39;Z axis\u0026#39;) # 显示图形 plt.show() 示例中，通过 plot_wireframe() 添加了网格线框，颜色映射使用了 plasma，通过 alpha=0.8 设置了透明度为 80%。\n效果如下所示：\n控制图形视角 # matplotlib 提供了对 3D 图形视角的控制。可以通过 ax.view_init() 来设置视角（即观察图形的角度），elev 参数设置仰角，azim 参数设置方位角。\n# 设置 60 仰角和 45 方位角 ax.view_init(elev=60, azim=45) 通过调整这些参数，您可以从不同的角度观察 3D 曲面图。\nMatplotlib 中绘制 3D 曲面图要点 # 创建数据网格：使用 numpy.meshgrid 生成二维的 X 和 Y 网格，并根据需要定义 Z 轴的值。 绘制曲面图：使用 matplotlib 的 plot_surface() 方法来绘制 3D 曲面，使用 cmap 来调整颜色映射。 自定义图形：可以添加透明度、线框，或者通过自定义函数来生成 Z 轴数据。同时，还可以通过 view_init() 调整视角。 可视化增强：为图形添加颜色条，调整坐标轴标签，使用不同的颜色映射函数来使数据更加清晰。 更多可用的颜色映射（colormap） # matplotlib 提供了丰富的颜色映射方案，您可以使用 cmap 参数来指定：\n'viridis'：默认色彩映射，适用于一般数据 'plasma'：对比度较高的配色方案 'inferno'：适合视觉对比 'coolwarm'：常用于正负值数据 例如：\nsurf = ax.plot_surface(X, Y, Z, cmap=\u0026#39;coolwarm\u0026#39;) 总结 # 使用 matplotlib 绘制 3D 曲面图帮助我们可视化复杂的三维数据。通过掌握基础的网格生成和绘图函数，以及对图形的进一步自定义和优化，就可轻松创建适合您需求的 3D 可视化图表。\n本文介绍了从基础的 3D 曲面图绘制方法，希望对你有所帮助。\n","date":"2024-09-18","externalUrl":null,"permalink":"/posts/2024-09-18-plot-3d-using-matplotlib-using-python/","section":"文章","summary":"在数据可视化中，3D 图表是一个非常有用的工具，特别是当我们想要展示复杂的三维数据时，如期权的波动率曲面。Python 的 matplotlib 库提供了生成各种类型图表，包括 3D 图表。\n","title":"如何使用 Python Matplotlib 绘制 3D 曲面图","type":"posts"},{"content":"本文介绍介绍一个 Python 包- opstrat，通过它绘制期权收益图，帮助我们理解期权策略的收益风险比。\n什么是期权收益图？ # 期权收益图展示了某个期权或期权组合的盈亏情况，如下图中，展示一张标的现价 100，行权价 102 的看涨期权合约。\n这种图表是理解风险与收益的重要工具，尤其是在期权的交易结构中，通过这些图表可以直观地看到交易的潜在利润与风险区间。\nOpstrat # Opstrat 是一个 Python 工具包，它提供了便捷的方法来可视化期权交易策略，无需复杂的编程知识。通过这个工具，交易者能够快速构建单个期权或者多个期权组合的收益图。\n安装 # 你可以通过以下命令安装 Opstrat：\npip install opstrat 该包会自动安装所需的依赖库，包括 pandas、matplotlib、seaborn 和 yfinance，这些都是用来处理数据和绘图的库。\n使用 Opstrat 进行期权可视化 # 安装完成后，你可以通过如下方式导入该包：\nimport opstrat as op 接下来，我们将展示如何使用 opstrat 绘制各种期权策略的收益图。\n绘制单个期权的收益图 # 要绘制单个期权的收益图，你可以使用 single_plotter() 函数。\n如绘制一张默认的看涨期权收益图：\nop.single_plotter() 默认生成一个看涨期权的收益图，现价为 100 元，行权价为 102 元。\n如下图所示：\n自定义单腿期权 # 通过提供参数，你可以自定义这个图表。例如，假设交易者卖出了行权价为 400 元的看跌期权，并收取了 12 元的期权费。以下代码将生成该卖出期权的收益图：\nop.single_plotter(spot=400, strike=400, op_type=\u0026#39;p\u0026#39;, tr_type=\u0026#39;s\u0026#39;, op_pr=12) 如下图：\n这个图表显示不同价格水平下的潜在盈利或亏损。tr_type 参数 s 表示 short 空这张 PUT 期权，如果为 b 表示是 buy。\n构建多腿期权组合 # 如果想要构建多个期权头寸的组合策略，例如卖出跨式或铁鹰策略，可以使用 multi_plotter() 函数。\n示例 1：卖出跨式策略 # 卖出跨式策略包括卖出略微虚值的看涨期权和看跌期权。\n在这个例子中，假设基础资产的现价为 100 元，交易者卖出了行权价分别为 110 元和 95 元的看涨期权和看跌期权。\nop_1 = {\u0026#39;op_type\u0026#39;:\u0026#39;c\u0026#39;,\u0026#39;strike\u0026#39;:110,\u0026#39;tr_type\u0026#39;:\u0026#39;s\u0026#39;,\u0026#39;op_pr\u0026#39;:2} op_2 = {\u0026#39;op_type\u0026#39;:\u0026#39;p\u0026#39;,\u0026#39;strike\u0026#39;:95,\u0026#39;tr_type\u0026#39;:\u0026#39;s\u0026#39;,\u0026#39;op_pr\u0026#39;:6} op.multi_plotter(spot=100, op_list=[op_1,op_2]) 如下图：\n图中即展示了单腿，也展示了组合的收益曲线。\n示例 2：铁鹰策略 # 更复杂的策略，如铁鹰策略，它结合了两个看涨期权和两个看跌期权。\n假设某只股票当前价格为 5000 元，交易者卖出行权价为 5100 元的看涨期权，同时买入行权价为 5200 元的看涨期权；同时，卖出行权价为 4900 元的看跌期权，买入行权价为 4800 元的看跌期权。\nop1={\u0026#39;op_type\u0026#39;: \u0026#39;c\u0026#39;, \u0026#39;strike\u0026#39;: 5100, \u0026#39;tr_type\u0026#39;: \u0026#39;s\u0026#39;, \u0026#39;op_pr\u0026#39;: 120} op2={\u0026#39;op_type\u0026#39;: \u0026#39;c\u0026#39;, \u0026#39;strike\u0026#39;: 5200, \u0026#39;tr_type\u0026#39;: \u0026#39;b\u0026#39;, \u0026#39;op_pr\u0026#39;: 80} op3={\u0026#39;op_type\u0026#39;: \u0026#39;p\u0026#39;, \u0026#39;strike\u0026#39;: 4900, \u0026#39;tr_type\u0026#39;: \u0026#39;s\u0026#39;, \u0026#39;op_pr\u0026#39;: 150} op4={\u0026#39;op_type\u0026#39;: \u0026#39;p\u0026#39;, \u0026#39;strike\u0026#39;: 4800, \u0026#39;tr_type\u0026#39;: \u0026#39;b\u0026#39;, \u0026#39;op_pr\u0026#39;: 100} op_list=[op1, op2, op3, op4] op.multi_plotter(spot=5000,spot_range=10, op_list=op_list) 如下图所示：\n这个策略适用于对基础资产价格波动不大的市场，在有限的风险下捕捉有限的利润。\n最后 # 通过 Opstrat 工具包，我们可以快速直观地构建各种期权策略的收益图，无论是简单的单腿期权，还是复杂的多腿期权组合。这提高了我们的分析效率，助我们理解期权交易的风险与收益。\n","date":"2024-09-09","externalUrl":null,"permalink":"/posts/2024-09-09-visualizing-option-strategy-using-python/","section":"文章","summary":"本文介绍介绍一个 Python 包- opstrat，通过它绘制期权收益图，帮助我们理解期权策略的收益风险比。\n什么是期权收益图？ # 期权收益图展示了某个期权或期权组合的盈亏情况，如下图中，展示一张标的现价 100，行权价 102 的看涨期权合约。\n","title":"Python 三方库 Opstrat 绘制收益图加深期权策略理解","type":"posts"},{"content":" 译文原文 - Covered Call Strategy in Python 本文介绍备兑期权（covered calls）策略，一个交易者员常用的策略。期权交易看起来复杂，常常使新手却步。然而，一旦正确掌握了，就会变得简单易懂。\n本博客中，我们将学习备兑期权策略的基础知识，并获取实用且易于理解的见解。我们还将讨论在 Python 中如何实现备兑期权策略，实现通过 Python 实现策略的可视化。这个过程中，我们将强调实践，同时确保深度，让每个部分能被理解。无论是希望拓展交易知识的新手，还是想要优化策略的老手，希望这篇博客都能对你有所帮助。\n期权交易概述 # 期权交易涉及到期权合约，这些合约赋予持有人在某个时间内以设定价格买入或卖出某项资产的权利，但不具备买卖的义务。期权有两种类型，即看涨期权和看跌期权。\n看涨期权：持有该期权的人有权（但无义务）在到期时以预定价格购买某项标的资产或股票。 看跌期权：持有该期权的人有权（但无义务）在到期时以预定价格卖出某项标的资产或股票。 期权交易的其他关键要素包括“多头”和“空头”，这两者指的是交易者在期权市场中的头寸。\n多头期权\n当交易者持有期权的“多头”头寸时，这意味着他们以某个特定的期权费买入期权合约（买入看涨期权或看跌期权）。这意味着交易者可以进行以下操作：\n买入看涨期权：买入标的资产的权利 买入看跌期权：卖出标的资产的权利 空头期权\n相反，当交易者持有期权的“空头”头寸时，意味着他们通过卖出期权合约赚取一定的期权费。这意味着交易者可以进行以下操作：\n卖出看涨期权：卖出标的资产的义务 卖出看跌期权：买入标的资产的义务 要进行期权交易，交易者通常根据市场展望选择策略，分析市场趋势，选择合适的行权价和到期日期，并通过券商平台进行交易，同时了解市场动态和风险管理。\n期权交易策略丰富，如常见的额买卖期权、跨式套利、宽跨式套利等。\n案例 # 假设你认为目前以每股 50 元交易的 XYZ 公司股票在下个月会涨价。你决定不直接买入股票，而是购买一份 XYZ 公司的看涨期权合约，行权价格为 55 元，合约到期日为一个月后。\n下面是此示例的策略分解：\n操作：你购买一份 XYZ 公司的看涨期权合约。 原因：你预期 XYZ 公司股票价格将会上涨。 购买看涨期权，你要支付了一定的期权费（如每股 2 元，一份合约计 200 元，合约规模 100 股）。这赋予你在到期日前以每股 55 元购买 100 股 XYZ 公司股票的权利，但非义务。\n如果 XYZ 公司的股价在下个月上涨至 55 元以上，你的看涨期权将最大化收益。例如，如果股价上涨至 60 元，你可以行使期权，以 55 元的价格购买股票，然后以市场价 60 元卖出股票，获得每股 5 元的利润（扣除购买期权时支付的期权费）。如果到期时股价未能突破 55 元，你可以让期权失效，只损失所支付的期权费。\n备兑期权策略 # 什么是备兑期权策略呢？备兑期权策略涉及拥有标的资产（如股票），并对这些资产卖出看涨期权。其操作方式如下：\n持有标的资产：首先，投资者持有特定股票。每份期权合约通常覆盖 100 股标的资产。\n卖出看涨期权：投资者随后卖出看涨期权，以此获得期权买方支付的期权费。\n有卖出的义务：当卖出看涨期权时，投资者同意在期权买方在到期日之前或到期时行权时，以指定的行权价出售所持股票。\n有限利润潜力：备兑期权策略的利润潜力限于卖出看涨期权时获得的期权费。如果股票价格低于期权的行权价，该期权通常会失效，投资者则保留期权费作为利润。\n提供下行保护：卖出看涨期权所获得的期权费为投资者提供了一些下行保护。它降低了标的资产的有效购买价格，提供了一定的缓冲，以应对股票价格下跌的潜在风险。\n机会成本风险：如果股票价格上涨超过看涨期权的行权价，投资者可能被迫以行权价出售所持股票，从而错过股价继续大幅上涨的潜在收益。\n备兑期权策略在对股票的短期前景持中性或略微看涨的情况下时受欢迎，通过该策略从股票持有中产生额外收入，同时可能降低股票的有效购买价格。\n案例学习 # 假设 XYZ 股票在 2023 年 4 月 29 日的交易价格为 189 元。现在，按照备兑期权策略，你将采取以下两种头寸：\n第一步：以 189 元的价格买入 100 股该股票。 第二步：卖出行权价为 195 的看涨期权，期权到期日为 2023 年 5 月 26 日。看涨期权的期权费为 6.30 元，合约规模为 100 股。 这两笔头寸的总支付金额等于股票价格减去卖出看涨期权获得的期权费，即 18,900 元（买入股票） – 630 元（期权费） = 18,270 元。\n如果股票价格上涨超过 195 的行权价，期权将被行权，股票将被卖出，而我们收到的期权费就是最大收益。\n而反之，如果到期时股票价格为 180 元，将可能导致损失，具体要计算下股票的波动损失是否大于获得的期权费。\n损失 = 获得的期权费 + ((股票到期价格 - 股票购买价格) x 股票数量)\n损失 = 630 元 + ((180 - 189) x 100) = -270 元\n如上图所示是备兑期权的收益图，即多头股票和卖出看涨期权的组合收益图。\n不同情况示例 # 具体分解下不同的情况下的收益或亏损，如持有 1 股价值 20 元的 ABC 股票，以 2 元的价格卖出行权价为 23 元的看涨期权。\n情况 1：如果股价涨至 20 \u0026lt;= P \u0026lt; 23 元，看涨期权失效，交易者赚取 2 元加上股票价格上涨的收益，总收益为 2 + (P - 20)。利润在 2 元到 5 元之间。 情况 2：如果股价涨至 P \u0026gt;= 23 元，看涨期权的买方将行使期权，以 23 元的价格从交易者处购买股票，因此交易者的净利润为 23 元 - 20 元 + 2 元 = 5 元。利润封顶为 5 元。 情况 3：如果股价跌至 P \u0026lt; 20 元，则看涨期权失效，交易者获得 2 元，而股票头寸的亏损为 2 元 + (P - 20 元)。利润/亏损区间从亏损 18 元到获得 2 元。 通过每月卖出看涨期权，投资者可以每月通过期权费获得收入。\n备兑期权的风险回报配置如下：\n最大风险：股票购买价格 - 期权费 最大收益：看涨期权行权价 - 股票购买价格 + 期权费 盈亏平衡：股票购买价格 - 期权费 现在已经了解了备兑期权策略的基础，我们将讨论何时使用该策略。\n何时使用 # 备兑期权策略在各种情景中被采用，主要是为了从股票持有中产生额外收入。\n通过对股票卖出看涨期权，投资者获得期权费，特别是在低波动性市场或股票预计保持稳定时，这种策略能够提升整体收益。当投资者对股票持中性或略微看涨的观点时，他们可以从期权费中获利，同时参与潜在的股价上涨。\n此外，投资者可以通过卖出行权价较高的看涨期权来从股票中获利。这在横盘市场中特别有效，期权费可能占到潜在回报的很大一部分，使其对收入产生非常有吸引力。\n然而，在实施备兑期权策略之前，必须仔细考虑市场条件、资产波动性和个人投资目标。\n接下来我们将讨论备兑期权策略的几种常见变化。\n备兑期权策略的变化 # 以下是几种最受欢迎的备兑期权策略变化：\n比例看涨期权卖出\n在这种策略中，投资者卖出超过所持股票数量的看涨期权。例如，他们可能为每持有的 100 股股票卖出两份看涨期权。该策略增加了收益潜力，但如果股票价格大幅上涨，投资者将面临更大的下行风险。\n备兑期权滚动\n当标的股票价格接近或超过卖出看涨期权的行权价时，投资者可以回购期权并卖出一份行权价更高且/或到期日更远的期权。\n这让他们能够获得额外的期权费收入，同时有可能从进一步的股价上涨中获益。\n对角线看涨期权价差\n投资者同时卖出一份较近到期的看涨期权，并买入一份行权价更高且到期日较远的看涨期权。该策略允许投资者通过期权费和标的股票的进一步上涨获利。\n备兑跨式期权\n与仅卖出看涨期权不同，投资者同时卖出看涨期权和看跌期权，以增加期权费收入。该策略在低波动性市场中效果显著。\n备兑领口期权\n投资者结合备兑期权策略与购买保护性看跌期权。该策略限制了标的股票的潜在损失，同时仍能通过卖出看涨期权获得收入。\n这些变化提供了根据市场状况、个人偏好和投资目标调整风险和回报的方法。但它们也伴随了不同程度的复杂性，可能需要进行仔细的监控和管理。\n接下来我们将讨论备兑期权策略在投资组合中的重要性。\n重要性 # 备兑期权策略在投资组合中的重要性体现在多个方面，如下是将备兑期权策略纳入投资组合的一些理由：\n收入来源：通过卖出看涨期权，获得稳定的收入流。这种收入能够提升整体投资组合的回报，特别是在低利率环境下。 风险管理：备兑期权提供了下行保护，通过减少标的股票的有效购买价格，期权费为股票价格下跌提供了缓冲。 增强回报：虽然收益有限，但在横盘或略微上涨的市场中，备兑期权策略的潜在利润仍能超越传统的买入持有策略。 灵活性：投资者可以根据自己的风险承受能力、市场展望和收入需求调整备兑期权策略，修改行权价、到期日和合约数量等变量。 投资组合多样化：通过将备兑期权策略纳入投资组合，投资者可以使投资组合的收入来源多样化，超越传统的股息或利息支付，从而降低整体投资组合的风险。 降低波动性：通过期权费产生的收入，备兑期权可以平滑投资组合的波动性，为投资者提供更稳定的收益曲线，特别是对于那些风险厌恶型的投资者。 总体而言，备兑期权策略提供了一种平衡的方式来产生收入和管理风险，使其成为投资者在寻求提升投资组合表现和有效管理风险时的重要工具。\n接下来我们将讨论使用 AAPL 股票进行备兑期权策略的真实案例。\n使用 AAPL 备兑期权策略的真实案例 # 让我们来看一下 2023 年初的 AAPL 案例，查看下面的历史数据，我们可以了解如何利用备兑期权策略。\n场景：在 2023 年 3 月至 6 月期间，一名交易者持有 100 股苹果公司（AAPL）的股票，当时股票价格在 145 美元到 180 美元间波动。\n假设他认为未来几个月股票价格可能保持不变或温和上涨。\n潜在的备兑期权策略：交易者决定对持有的 AAPL 股票卖出行权价为 200 美元、到期日为六个月后的看涨期权。当时该看涨期权的期权费约为每股 5 美元，这意味着她通过卖出备兑期权可获得 500 美元（100 股 * 5 美元期权费）。\n结果：\n回顾历史数据，AAPL 股票价格在接下来的六个月内确实没有突破 200 元，这就是一个备兑期权策略如何为投资者产生收入的实例，同时如果股价没有上涨过多，投资者还可以继续持有股票。\n接下来，我们将在 Python 中逐步实现备兑期权策略可视化收益曲线。\n如何在 Python 中逐步实现备兑期权策略 # 下面是使用 Python 代码绘制多头股票、卖出看涨期权和备兑策略的收益图表。\n首先是收益计算：\n# 备兑期权 import numpy as np import matplotlib.pyplot as plt s0=189 # 初始股票价格 k=195; c=6.30; # 期权行权价和期权费 shares = 100 # 每张合约的股票数量 sT = np.arange(0, 2*s0, 5) # 到期时的股票价格 # 多头股票的收益/损失 y1= (sT-s0) * shares # 卖出看涨期权的收益 y2 = np.where(sT \u0026gt; k,((k - sT) + c) * shares, c * shares) # 备兑期权的收益 y3 = np.where(sT \u0026gt; k,((k - s0) + c) * shares,((sT - s0) + c) * shares ) 我们使用 matplotlib 库来绘制图表，去除上边界和右边界，并将 X 轴移至中心。使用 plt.plot 函数绘制多头股票、卖出看涨期权和备兑期权的收益图表。\n# 使用 matplotlib 创建图表 fig, ax = plt.subplots() ax.spines[\u0026#39;top\u0026#39;].set_visible(False) # 隐藏顶部边界 ax.spines[\u0026#39;right\u0026#39;].set_visible(False) # 隐藏右侧边界 ax.spines[\u0026#39;bottom\u0026#39;].set_position(\u0026#39;zero\u0026#39;) # 设置 X 轴居中 ax.tick_params(top=False, right=False) # 移除右侧的刻度标记 plt.plot(sT,y1,lw=1.5,label=\u0026#39;Long Stock\u0026#39;) plt.plot(sT,y2,lw=1.5,label=\u0026#39;Short Call\u0026#39;) plt.plot(sT,y3,lw=1.5,label=\u0026#39;Covered Call\u0026#39;) 最后，我们在图表中添加标题、标签和图例。\nplt.title(\u0026#39;Covered Call\u0026#39;) plt.xlabel(\u0026#39;Stock Prices\u0026#39;) plt.ylabel(\u0026#39;Profit/loss\u0026#39;) plt.grid(True) plt.axis(\u0026#39;tight\u0026#39;) plt.legend(loc=0) plt.show() 输出：\n接下来，我们将简单说下备兑期权策略的风险管理技巧，这些技巧可以助你最大化收益。\n备兑期权策略的风险管理技巧 # 风险管理在实施备兑期权策略时至关重要。以下是管理风险的具体技巧：\n行权价选择：选择符合你风险承受能力和市场展望的行权价。卖出行权价远高于当前股价的看涨期权可能带来更高的期权费，但也增加了在期权被行权时面临的潜在损失。 到期日管理：选择既能产生收入又具有灵活性的到期日。较长的到期日通常提供更高的期权费，但也可能增加调整策略的难度。 多样化：在不同的股票、行业或 ETF 之间分散备兑期权头寸，以减少集中风险。避免过度暴露于单一股票或行业，这会增加因不利价格变动带来的风险。 监控与调整：定期监控备兑期权头寸的表现，并在必要时进行调整。如果股票价格接近或超过看涨期权的行权价，考虑回购期权并卖出一份行权价更高或到期日更远的期权，以获取更多期权费收入并可能减少损失。 降低风险的策略：考虑使用保护性看跌期权或领口策略来对冲潜在的下行风险。保护性看跌期权为股票价格下跌提供了保险，而领口策略则通过同时买入看跌期权和卖出备兑看涨期权来限制潜在的损失和收益。 仓位管理：根据你的整体投资组合规模和风险承受能力管理备兑期权头寸的规模。避免在波动性市场中过度投入备兑期权策略，以保持流动性和灵活性。 市场分析：在进入备兑期权头寸之前，进行深入的市场趋势、波动性水平和个股基本面分析。关注经济指标、公司财报公告和可能影响股票价格和期权费的其他因素。 通过采用这些风险管理技巧，投资者可以有效降低与备兑期权策略相关的风险，并优化收入增长和资本保值的潜力。\n结论 # 掌握在 Python 中的备兑期权策略为探索期权交易复杂性提供了强大的工具。我们讨论了交易者如何利用这些知识和技能来有效地实施备兑期权策略，从理解基础知识到使用 Python 代码的实际应用。\n最终，掌握在 Python 中的备兑期权策略可以为交易者打开机会之门，提升投资组合表现、产生收入并实现财务目标。拥有正确的知识和工具，你可以自信且精准地在动态的期权交易世界中游刃有余。\n","date":"2024-09-08","externalUrl":null,"permalink":"/posts/2024-09-08-covered-call-strategy-in-python/","section":"文章","summary":" 译文原文 - Covered Call Strategy in Python 本文介绍备兑期权（covered calls）策略，一个交易者员常用的策略。期权交易看起来复杂，常常使新手却步。然而，一旦正确掌握了，就会变得简单易懂。\n","title":"期权备兑策略介绍与通过 Python 绘制收益图","type":"posts"},{"content":" 引言部分 # 在数据分析中，周期转换是一个常见的需求，尤其是在处理时间序列数据时。Python 提供了一个非常强大的工具 —— resample 函数，用于轻松地对时间序列数据进行重新采样。比如，将日线数据转换为周线数据，或者将分钟线数据转换为小时线数据。\n今天，我们将详细介绍如何使用 Python 的 pandas 库中的 resample 函数，将日线数据转换为周线数据，并展示如何处理一些常见的数据问题。\n基础知识和思路部分 # 时间序列数据：\n时间序列数据是指按时间顺序排列的数据，常用于表示价格波动、气温变化等。在金融市场中，蜡烛图是非常常见的时间序列数据。每根蜡烛图通常代表一个特定时间段（如一分钟、一天、一周）的价格数据。\n要将一个较短周期的时间序列（如日线数据）转换为较长周期的时间序列（如周线数据），我们需要使用周期转换的方法。\nresample 函数的基本概念：\n在 pandas 中，resample 函数是用来重新采样时间序列数据的强大工具。它可以将时间序列数据从一个时间频率转换为另一个时间频率。例如，使用 resample('W') 可以将日线数据转换为周线数据，使用 resample('M') 可以将日线数据转换为月线数据。\nresample 函数的基本语法如下：\ndf.resample(\u0026#39;时间频率\u0026#39;).聚合函数() 时间频率：指定目标频率，如 'D'（日）、'W'（周）、'M'（月）、'H'（小时）等。 聚合函数：指定如何对数据进行聚合（例如：first、last、mean、sum、max、min等）。 使用 resample 函数进行周期转换 # 接下来，我们将使用 pandas 的 resample 函数将日线数据转换为周线数据。我们以南华期货指数的数据为例，展示如何使用 resample 完成这一任务。\n代码实现：\nimport tushare as ts import pandas as pd # 获取数据 pro = ts.pro_api() df = pro.index_daily(\u0026#34;C.NH\u0026#34;) # 将日期列转换为日期时间类型 df[\u0026#39;trade_date\u0026#39;] = pd.to_datetime(df[\u0026#39;trade_date\u0026#39;]) # 按日期排序 df.sort_values(by=\u0026#39;trade_date\u0026#39;, inplace=True) # 设置日期列为索引 df.set_index(\u0026#39;trade_date\u0026#39;, inplace=True) # 将日线数据转换为周线数据 weekly_data = df.resample(\u0026#39;W\u0026#39;).agg({ \u0026#39;open\u0026#39;: \u0026#39;first\u0026#39;, # 每周的开盘价取该周第一个交易日的开盘价 \u0026#39;high\u0026#39;: \u0026#39;max\u0026#39;, # 每周的最高价取该周内所有交易日的最高价 \u0026#39;low\u0026#39;: \u0026#39;min\u0026#39;, # 每周的最低价取该周内所有交易日的最低价 \u0026#39;close\u0026#39;: \u0026#39;last\u0026#39; # 每周的收盘价取该周最后一个交易日的收盘价 }) print(weekly_data) resample 函数的常见用法 # 在实际使用中，resample 函数支持很多灵活的用法。我们将展示一些常见的使用案例，帮助你更好地掌握 resample 的使用技巧。\n1. 将日线数据转换为周线数据 # 这是最基础的操作，用来将较短周期的数据转换为较长周期的数据。例如，将日线数据转换为周线数据时，我们通过以下步骤来计算每周的开盘价、最高价、最低价和收盘价：\nweekly_data = df.resample(\u0026#39;W\u0026#39;).agg({ \u0026#39;open\u0026#39;: \u0026#39;first\u0026#39;, \u0026#39;high\u0026#39;: \u0026#39;max\u0026#39;, \u0026#39;low\u0026#39;: \u0026#39;min\u0026#39;, \u0026#39;close\u0026#39;: \u0026#39;last\u0026#39; }) 2. 将日线数据转换为月线数据 # 如果你想将日线数据转换为月线数据，只需要将 'W' 修改为 'M'，就可以得到每个月的开盘价、最高价、最低价和收盘价：\nmonthly_data = df.resample(\u0026#39;M\u0026#39;).agg({ \u0026#39;open\u0026#39;: \u0026#39;first\u0026#39;, \u0026#39;high\u0026#39;: \u0026#39;max\u0026#39;, \u0026#39;low\u0026#39;: \u0026#39;min\u0026#39;, \u0026#39;close\u0026#39;: \u0026#39;last\u0026#39; }) 3. 按小时聚合数据 # 如果你有分钟线数据，想将其聚合为小时线数据，可以使用 resample('H') 来实现：\nhourly_data = df.resample(\u0026#39;H\u0026#39;).agg({ \u0026#39;open\u0026#39;: \u0026#39;first\u0026#39;, \u0026#39;high\u0026#39;: \u0026#39;max\u0026#39;, \u0026#39;low\u0026#39;: \u0026#39;min\u0026#39;, \u0026#39;close\u0026#39;: \u0026#39;last\u0026#39; }) 4. 对数据进行其他聚合操作 # 除了基本的 first、last、max、min 等聚合函数外，resample 还支持其他一些常见的聚合操作：\nmean：计算时间段内的平均值。 sum：计算时间段内的总和。 ohlc：用于金融数据，返回开盘价、最高价、最低价和收盘价。 例如，计算每月的平均价格：\nmonthly_avg_data = df.resample(\u0026#39;M\u0026#39;).mean() 处理缺失数据 # 在使用 resample 时，有时会遇到一些缺失的数据。比如，某些周没有交易数据，可能会导致某些周的数据缺失。我们可以通过 dropna() 删除这些缺失的数据，或者使用 fillna() 填充缺失值。\n# 删除缺失数据 weekly_data.dropna(inplace=True) # 或者填充缺失数据 # weekly_data.fillna(method=\u0026#39;ffill\u0026#39;, inplace=True) # 向前填充 结论部分 # 通过 pandas 的 resample 函数，我们可以非常方便地对时间序列数据进行周期转换。这不仅能帮助我们更清晰地观察不同时间尺度上的市场趋势，还能为进一步的数据分析提供更高效的支持。\n希望这篇文章能帮助你更好地理解 resample 函数的使用，并在实际工作中能够灵活应用。如果你对 Python 数据分析感兴趣，继续深入学习 pandas 和其他数据处理工具，会让你在数据分析的道路上更加得心应手。\n总结 # 在这篇文章中，我们详细讲解了如何通过 Python 的 resample 函数将日线数据转换为周线数据，并展示了 resample 的常见使用方法。通过灵活应用 resample，你可以轻松地处理不同周期的数据，帮助你更好地分析时间序列数据。\n","date":"2024-07-23","externalUrl":null,"permalink":"/posts/2024-07-23-conver-daily-ohlc-to-weekly/","section":"文章","summary":"引言部分 # 在数据分析中，周期转换是一个常见的需求，尤其是在处理时间序列数据时。Python 提供了一个非常强大的工具 —— resample 函数，用于轻松地对时间序列数据进行重新采样。比如，将日线数据转换为周线数据，或者将分钟线数据转换为小时线数据。\n","title":"如何使用 Python 将日线行情转换为周线","type":"posts"},{"content":"在交易中，如果你有一套人工配合机器人的交易思路，即希望人工确认而不直接下单，那么及时获取关键位置的通知信息，如一些特征明显的技术指标的告警信息，是很有帮助的。\n市面上有不少的付费软件支持这个能力，如果你可以使用 TradingView，它的免费版也可以做到，缺点是缺少国内期货品种。我是不想花钱的，秉承着自己动手，丰衣足食，毕竟小散户不是大机构。\n本文将基于开源数据方案，通过 Python 一套免费的方案，编写指标告警并将其发送到邮件。我将利用 akshare 获取历史行情数据，使用 TA-Lib 计算技术指标。\n接下来将按照这个流程逐步展开介绍。\n数据下载 # 在上一篇文章中，我们介绍了如何使用 akshare 下载国内期货、股票和指数的历史行情。假设，我们已经下载了数据，将其存储在一个 DataFrame 中。\n如下是上证指数 000001 的数据下载，我们可直接使用上篇文章的 history_bars 方法，并将数据保存到 index_000001.csv 文件中。\n# 下载上证指数的历史行情数据 df = history_bars(symbol=\u0026#34;000001\u0026#34;, length=100) df.to_csv(\u0026#34;index_000001.csv\u0026#34;, index=False) 提前下载行情是为了防止重复调用产生可能的网络问题，把行情查询和指标计算分离，提高报警程序稳定性。\n安装 TA-Lib 库 # 现在可以开始计算指标了。\npython 的指标计算库有如 talib、pandas-ta、ta 等等，其中 talib 最出名，支持超过 150 中技术指标，由 C 语言实现，性能更高。我将使用的就是 talib。\nTA-Lib 的安装要提前安装依赖的动态库，参考 TA-Lib 官方文档。我简单展开说说不同操作系统下依赖库的安装方法吧。\nmacOS\nmacOS 上可通过 Homebrew 安装：\nbrew install ta-lib Linux\nLinux 上要手动编译安装，提前下载 ta-lib-0.4.0-src.tar.gz，并执行如下命令：\n$ wget https://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz $ tar zxvf ta-lib-0.4.0-src.tar.gz $ cd ta-lib/ $ ./configure --prefix=/usr $ make $ sudo make install Window\nWindows 上下载 ta-lib-0.4.0-msvc.zip 文件，并将其解压到 C:\\ta-lib。\n我记得 windows 上也是可以直接下载 wheel 文件安装 ta-lib，我很早之前使用 windows 就是通过这种方式安装的。\npip install\n在依赖库安装完成后，直接 pip 即可安装 TA-Lib。\npip install ta-lib 到此，TA-Lib 就安装成功了。有不少人开始入手指标计算的时候，都被 TA-Lib 的安装给卡住了，希望以上的介绍对你有用。\n指标计算与告警 # 我们开始计算指标和告警规则，我会用 RSI 和布林带这两个耳熟能详的指标，基于它们配置一些报警规则。\n首先加载 index_000001.csv 中保存的数据，进行清洗和预处理，便于指标计算。\ndf = pd.read_csv(\u0026#34;index_000001.csv\u0026#34;) df[\u0026#39;date\u0026#39;] = pd.to_datetime(df[\u0026#39;date\u0026#39;]) df.set_index(\u0026#39;date\u0026#39;, inplace=True) 接下来，使用 TA-Lib 计算 RSI 和布林带（BollingerBands）。\nimport talib # 计算 RSI df[\u0026#39;RSI\u0026#39;] = talib.RSI(df[\u0026#39;close\u0026#39;], timeperiod=14) # 计算布林带 df[\u0026#39;upperband\u0026#39;], df[\u0026#39;middleband\u0026#39;], df[\u0026#39;lowerband\u0026#39;] = talib.BBANDS(df[\u0026#39;close\u0026#39;], timeperiod=20) 如果想了解更多技术指标的计算方法，可直接查看 TA-Lib 的文档，它还可以计算其他常见的技术指标，例如 MACD 和移动平均线。\n# 计算 MACD df[\u0026#39;macd\u0026#39;], df[\u0026#39;macdsignal\u0026#39;], df[\u0026#39;macdhist\u0026#39;] = talib.MACD(df[\u0026#39;close\u0026#39;]) # 计算移动平均线 df[\u0026#39;SMA_50\u0026#39;] = talib.SMA(df[\u0026#39;close\u0026#39;], timeperiod=50) df[\u0026#39;SMA_200\u0026#39;] = talib.SMA(df[\u0026#39;close\u0026#39;], timeperiod=200) 基本在市面上常见的指数指标在 TA-Lib 中都可以找到，查看它的 指标函数列表。\n有了指标后，开始定义报警条件。我为了演示，定义 RSI 和布林带的告警条件为常用的超买信号，即 RSI 大于 70 和收盘价大于布林带上轨为报警条件。\n基于如下代码：\nrsi_alerts = df[\u0026#39;RSI\u0026#39;] \u0026gt; 70 # RSI 超过 70 bollinger_alerts = df[\u0026#39;close\u0026#39;] \u0026gt; df[\u0026#39;upperband\u0026#39;] # 收盘价超过上轨 有了两条告警后，设置为每天收盘后运行，决定第二天是否交易。\n或许，你希望收盘临近时检测，从而在收盘前提前入场，不过这个前提是要能获取到实时行情，akshare 中也提供了这样的接口，可查看它的 接口列表 查找。\n发送告警信息 # 告警规则有了，将信息发送到我们就变得非常重要了，我选择的发送到邮件。我将其封装消息发送函数。\nsend_email(title, body, to_email) 使用 Python 的 smtplib 库可以轻松发送邮件。当然，你需要提前开始 smtp 支持，如 163 邮箱，登录到邮箱的设置页 -\u0026gt; POP3/SMTP/IMAP，启用 POP3/SMTP 服务，会得到一串授权码，而 163 的 smtp 的地址和 SSL 的端口分别是smtp.163.com 和 465。\nimport smtplib from email.mime.text import MIMEText def send_email(subject, body, to_email): from_email = \u0026#39;your_email@163.com\u0026#39; from_password = \u0026#39;your_email_password\u0026#39; # smtp 的授权码 msg = MIMEText(body) msg[\u0026#39;Subject\u0026#39;] = subject msg[\u0026#39;From\u0026#39;] = from_email msg[\u0026#39;To\u0026#39;] = to_email server = smtplib.SMTP_SSL(\u0026#39;smtp.163.com\u0026#39;, 465) server.login(from_email, from_password) server.sendmail(from_email, [to_email], msg.as_string()) server.quit() 只要一行代码即可测试，如下：\nsend_email(\u0026#34;你好\u0026#34;, \u0026#34;测试邮件\u0026#34;, \u0026#34;to_email@163.com\u0026#34;) 万事具备，我们现在可以发送告警信息了。\nalerts = rsi_alerts | bollinger_alerts if alerts.iloc[-1] \u0026lt; 50: last_close = df[\u0026#34;close\u0026#34;].iloc[-1] last_rsi = df[\u0026#34;RSI\u0026#34;].iloc[-1] last_upperband = df[\u0026#34;upperband\u0026#34;].iloc[-1] message = f\u0026#34;告警！RSI：{last_rsi}，收盘价：{last_close}，上轨：{last_upperband}\u0026#34; send_email(\u0026#34;告警！\u0026#34;, message, \u0026#34;poloxue123@gmail.com\u0026#34;) 实现的逻辑还是非常简单的，当然告警的条件要视你的交易思路来实现。\n示例项目 # 以下是一个完整代码，将上面的过程整合到了一起、指标计算到发送告警信息的全过程：\nimport pandas as pd import talib import smtplib from email.mime.text import MIMEText df = pd.read_csv(\u0026#34;index_000001.csv\u0026#34;) df[\u0026#34;date\u0026#34;] = pd.to_datetime(df[\u0026#34;date\u0026#34;]) df.set_index(\u0026#34;date\u0026#34;, inplace=True) # 计算 RSI df[\u0026#34;RSI\u0026#34;] = talib.RSI(df[\u0026#34;close\u0026#34;], timeperiod=14) # 计算布林带 df[\u0026#34;upperband\u0026#34;], df[\u0026#34;middleband\u0026#34;], df[\u0026#34;lowerband\u0026#34;] = talib.BBANDS( df[\u0026#34;close\u0026#34;], timeperiod=20 ) # 计算 MACD # df[\u0026#39;macd\u0026#39;], df[\u0026#39;macdsignal\u0026#39;], df[\u0026#39;macdhist\u0026#39;] = talib.MACD(df[\u0026#39;close\u0026#39;]) # 计算移动平均线 # df[\u0026#39;SMA_50\u0026#39;] = talib.SMA(df[\u0026#39;close\u0026#39;], timeperiod=50) # df[\u0026#39;SMA_200\u0026#39;] = talib.SMA(df[\u0026#39;close\u0026#39;], timeperiod=200) rsi_alerts = df[\u0026#34;RSI\u0026#34;] \u0026gt; 70 # RSI 超过 70 bollinger_alerts = df[\u0026#34;close\u0026#34;] \u0026gt; df[\u0026#34;upperband\u0026#34;] # 收盘价超过上轨 PASSWORD = \u0026#34;your_email_password\u0026#34; def send_email(subject, body, to_email): from_email = \u0026#34;your_email@163.com\u0026#34; from_password = PASSWORD msg = MIMEText(body) msg[\u0026#34;Subject\u0026#34;] = subject msg[\u0026#34;From\u0026#34;] = from_email msg[\u0026#34;To\u0026#34;] = to_email server = smtplib.SMTP_SSL(\u0026#34;smtp.163.com\u0026#34;, 465) server.login(from_email, from_password) server.sendmail(from_email, [to_email], msg.as_string()) server.quit() alerts = rsi_alerts | bollinger_alerts if alerts.iloc[-1] \u0026lt; 50: last_close = df[\u0026#34;close\u0026#34;].iloc[-1] last_rsi = df[\u0026#34;RSI\u0026#34;].iloc[-1] last_upperband = df[\u0026#34;upperband\u0026#34;].iloc[-1] message = f\u0026#34;告警！RSI：{last_rsi}，收盘价：{last_close}，上轨：{last_upperband}\u0026#34; send_email(\u0026#34;告警！\u0026#34;, message, \u0026#34;to_email@163.com\u0026#34;) 注意将以上的邮件地址和授权码替换为真实的即可。\n定时执行 # 现在，我们只要配置脚本的定时执行即可，可以使用 python 的 schedule 包，或是在 Linux 系统上，直接通过 Crontab 配置定时任务。\n演示案例：如配置周一到周五，每天9点检查并告警。\n如果使用 schedule，要讲如上的过程封装下便于调用。\nimport time import datetime import schedule def indicator_alerts(): # 你的检测代码 pass # 配置周一到周五的早上九点执行任务 schedule.every().monday.at(\u0026#34;09:00\u0026#34;).do(indicator_alerts) schedule.every().tuesday.at(\u0026#34;09:00\u0026#34;).do(indicator_alerts) schedule.every().wednesday.at(\u0026#34;09:00\u0026#34;).do(indicator_alerts) schedule.every().thursday.at(\u0026#34;09:00\u0026#34;).do(indicator_alerts) schedule.every().friday.at(\u0026#34;09:00\u0026#34;).do(indicator_alerts) while True: schedule.run_pending() time.sleep(1) 如果是 Crontab，直接配置定时任务即可，如下所示：\n0 9 * * 1-5 python your_script.py 现在，我们就可以实现对基于技术指标的告警配置，还能通过邮件接收告警信息。\n希望这篇文章对你有所帮助。如果有任何问题或建议，欢迎留言讨论。\n","date":"2024-07-14","externalUrl":null,"permalink":"/posts/2024-07-14-technical-indicator-email-alerts/","section":"文章","summary":"在交易中，如果你有一套人工配合机器人的交易思路，即希望人工确认而不直接下单，那么及时获取关键位置的通知信息，如一些特征明显的技术指标的告警信息，是很有帮助的。\n","title":"Python 实现技术指标邮件告警通知","type":"posts"},{"content":"本文介绍如何使用 akshare 封装一个易用的下载国内期货、期货指数、股票和股票指数的历史行情数据接口。\n目标接口的定义如下所示：\nhistory_bars(symbol, length=100, equity_type=\u0026#34;future\u0026#34;) 这么封装也是为了便于使用。\n如果不了解 akshare，我已经写过一篇关于 akshare 下载金融数据的文章：通过 python 获取金融数据-akshare。\n安装 Akshare # 请确保已安装 Akshare，如下命令安装：\npip install akshare 了解它的更多数据，可查看它的文档：akshare documentation\n下载期货数据 # akshare 中的主力和连续合约的数据是从新浪财经下载的，要请求行情接口，首先要知道品种对应的 symbol 名称。它的 symbol 名称可以通过函数 futures_display_main_sina 拿到映射关系。\nimport akshare as aks print(aks.futures_display_main_sina()) 输出：\nsymbol exchange name 0 V0 dce PVC连续 1 P0 dce 棕榈油连续 2 B0 dce 豆二连续 3 M0 dce 豆粕连续 4 I0 dce 铁矿石连续 .. ... ... ... 72 IC0 cffex 中证500指数期货连续 73 TS0 cffex 2年期国债期货连续 74 IM0 cffex 中证连续指数期货连续 75 SI0 gfex 工业硅连续 76 LC0 gfex 碳酸锂连续 [77 rows x 3 columns] 现在将 symbol 传递给 futures_main_sina 函数即可下载合约的历史数据。\n如上的 C0 表示的是玉米主力合约，下载它的日线行情：\ndata = aks.futures_main_sina( symbol=\u0026#34;C0\u0026#34;, start_date=\u0026#34;20230101\u0026#34; ) 为便于统一封装到 history_bars，我们定义一个函数 future_history_bars，参数是 symbol 名称和长度，将输出结果转换为易读的格式。\ndef future_history_bars(symbol, length=100): start_date = datetime.now() - timedelta(days=length * 2) data = aks.futures_main_sina( symbol=symbol, start_date=start_date.strftime(\u0026#34;%Y%m%d\u0026#34;) ) data = data.rename( columns={ \u0026#34;日期\u0026#34;: \u0026#34;date\u0026#34;, \u0026#34;开盘价\u0026#34;: \u0026#34;open\u0026#34;, \u0026#34;最高价\u0026#34;: \u0026#34;high\u0026#34;, \u0026#34;最低价\u0026#34;: \u0026#34;low\u0026#34;, \u0026#34;收盘价\u0026#34;: \u0026#34;close\u0026#34;, \u0026#34;成交量\u0026#34;: \u0026#34;volume\u0026#34;, \u0026#34;持仓量\u0026#34;: \u0026#34;open_interest\u0026#34;, \u0026#34;动态结算价\u0026#34;: \u0026#34;settle_price\u0026#34;, } ) data[\u0026#34;open\u0026#34;] = data[\u0026#34;open\u0026#34;].astype(float) data[\u0026#34;high\u0026#34;] = data[\u0026#34;high\u0026#34;].astype(float) data[\u0026#34;low\u0026#34;] = data[\u0026#34;low\u0026#34;].astype(float) data[\u0026#34;close\u0026#34;] = data[\u0026#34;close\u0026#34;].astype(float) return data.iloc[-length:] 上面的参数和输出的定义看个人习惯，我比较习惯这样的使用方式，便于我计算指标。\n提醒：\n上述的按 length 获取数据的方式没有按交易日历来，只是粗略地从当前时间往前推 2*length 个周期得到开始时间 start_date，如果 length 太小，例如两个周期，如果赶上假期，就可能获取不到数据，这点要注意的。\n如果想长度精确，最简单的方式就是把数据存入数据库，通过 SOL 加载。或者是拿到交易日历，按日历计算交易天数。\n南华期货指数 # 我最初封装这个接口是为了回测策略，但发现期货主连数据是未复权的数据，没有考虑换仓，不符合真实交易场景。\n有没有满足需求的数据？\n当然有！市面上有南华期货指数，它考虑了换仓的场景，经过复权编制出的数据，符合真实交易场景，可用来回测。\nakshare 的文档中提供了南华指数的接口，我测试了下。很遗憾，几个接口都不能用了。\n# aks.futures_index_symbol_table_nh() 南华 symbol 表格 # aks.futures_price_index_nh() 南华价格指数 # aks.futures_return_index_nh() 南华收益指数 我本希望这篇文章中都是免费数据，但确实没有找到好的渠道。如果确有需要，可从 tushare 下载，访问 南华期货指数日线行情。\n除了使用南华期货指数，也可自己计算，前提是知道主连合约的换仓时间。在 akshare 上没发现这个数据，如需要，也可从 tushare 下载，访问期货主力与连续合约，其中包含了主连合约每天映射的实际合约。\n将 tushare 的南华指数行情封装到成 history_bars 的形式。\ndef nanhua_future_history_bars(symbol, length=100): start_date = (datetime.now() - timedelta(days=length * 2)).strftime(\u0026#34;%Y%m%d\u0026#34;) end_date = datetime.now().strftime(\u0026#34;%Y%m%d\u0026#34;) pro = ts.pro_api() data = pro.index_daily(ts_code=symbol, start_date=start_date, end_date=end_date) if not len(data): return data.dropna(inplace=True) data.rename(columns={\u0026#34;trade_date\u0026#34;: \u0026#34;date\u0026#34;, \u0026#34;vol\u0026#34;: \u0026#34;volume\u0026#34;}, inplace=True) data.sort_values(by=\u0026#34;date\u0026#34;) data[\u0026#34;date\u0026#34;] = pd.to_datetime(data[\u0026#34;date\u0026#34;]) data.set_index(\u0026#34;date\u0026#34;, inplace=True) return data 下载股票数据 # A 股股票数据可使用 stock_zh_a_hist 下载，它的输入参数包括股票代码、周期、复权方式和起始日期等参数。\ndata = aks.stock_zh_a_hist( symbol=\u0026#34;000001\u0026#34;, period=\u0026#34;daily\u0026#34;, adjust=\u0026#34;qfq\u0026#34;, start_date=\u0026#34;20230101\u0026#34; ) 我们的习惯，一般默认前复权即可。\n为了使用方便，同样可定义一个函数，易于使用：\ndef stock_history_bars(symbol, length=100): start_date = datetime.now() - timedelta(days=length * 2) data = aks.stock_zh_a_hist( symbol=symbol, period=\u0026#34;daily\u0026#34;, adjust=\u0026#34;qfq\u0026#34;, start_date=start_date.strftime(\u0026#34;%Y%m%d\u0026#34;), ) return data.rename( columns={ \u0026#34;日期\u0026#34;: \u0026#34;date\u0026#34;, \u0026#34;股票代码\u0026#34;: \u0026#34;symbol\u0026#34;, \u0026#34;开盘\u0026#34;: \u0026#34;open\u0026#34;, \u0026#34;最高\u0026#34;: \u0026#34;high\u0026#34;, \u0026#34;最低\u0026#34;: \u0026#34;low\u0026#34;, \u0026#34;收盘\u0026#34;: \u0026#34;close\u0026#34;, \u0026#34;成交量\u0026#34;: \u0026#34;volume\u0026#34;, } )[ [ \u0026#34;date\u0026#34;, \u0026#34;symbol\u0026#34;, \u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;, \u0026#34;volume\u0026#34;, ] ].iloc[ -length: ] 我们可以定义一个函数，下载特定指数的历史数据，并将其转换为更易读的格式：\n下载指数数据 # A 股指数数据可通过 index_zh_a_hist 函数实现。输入参数包括指数代码、周期和起始日期等参数。\ndata = aks.index_zh_a_hist( symbol=\u0026#34;000001\u0026#34;, period=\u0026#34;daily\u0026#34;, start_date=\u0026#34;20230101\u0026#34;, end_date=\u0026#34;20231231\u0026#34; ) 将其实现为 history_bar 函数的形式，便于我的使用。如下所示：\ndef index_history_bars(symbol, length=100): start_date = (datetime.now() - timedelta(days=length * 2)).strftime(\u0026#34;%Y%m%d\u0026#34;) end_date = datetime.now().strftime(\u0026#34;%Y%m%d\u0026#34;) data = aks.index_zh_a_hist( symbol=symbol, period=\u0026#34;daily\u0026#34;, start_date=start_date, end_date=end_date, ) return data.rename( columns={ \u0026#34;日期\u0026#34;: \u0026#34;date\u0026#34;, \u0026#34;开盘\u0026#34;: \u0026#34;open\u0026#34;, \u0026#34;最高\u0026#34;: \u0026#34;high\u0026#34;, \u0026#34;最低\u0026#34;: \u0026#34;low\u0026#34;, \u0026#34;收盘\u0026#34;: \u0026#34;close\u0026#34;, \u0026#34;成交量\u0026#34;: \u0026#34;volume\u0026#34;, } )[ [ \u0026#34;date\u0026#34;, \u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;, \u0026#34;volume\u0026#34;, ] ].iloc[ -length: ] 现在，我们就已经有了三种不同交易品种的历史数据获取方法。\n主函数 # 最后，还可以定义一个主函数，根据输入的类型下载相应的历史数据：\ndef history_bars(symbol, length=100, equity_type=\u0026#34;stock\u0026#34;): if equity_type == \u0026#34;stock\u0026#34;: return stock_history_bar(symbol, length=length) elif equity_type == \u0026#34;future\u0026#34;: return future_history_bar(symbol, length=length) elif equity_type == \u0026#34;nan_future\u0026#34;: return nanhua_future_history_bar(symbol, length=length) elif equity_type == \u0026#34;index\u0026#34;: return index_history_bar(symbol, length=length) else: raise ValueError(f\u0026#34;Unsupported equity type: {equity_type}\u0026#34;) 现在，是不是使用起来就非常方便了。\n示例使用 # 通过以下示例演示如何调用这些函数来获取不同品种的历史行情数据：\n获取上证数据数据：\nsymbol = \u0026#34;000001\u0026#34; # 上证指数 length = 100 equity_type = \u0026#34;index\u0026#34; data = history_bars(symbol, length, equity_type) print(data) 获取平安银行数据：\nsymbol = \u0026#34;000001\u0026#34; # 平安银行 length = 100 equity_type = \u0026#34;stock\u0026#34; data = history_bars(symbol, length, equity_type) print(data) 获取甲醇行情数据：\nsymbol = \u0026#34;MA0\u0026#34; # 甲醇主连 length = 100 equity_type = \u0026#34;stock\u0026#34; data = history_bars(symbol, length, equity_type) print(data) 通过以上代码，现在就有了轻松下载股票、指数和期货历史数据的能力。这为数据分析提供了有力的支持。\nhistory_bars 的代码请查看 hist_data.py。\n现在有了数据，就可以实现任何其他我们像实现的事情，如计算指标发送告警，绘制图表，如果有交易 API 接口，可直接报单，不过这个前提是有了好的策略。\n指标计算 # 简单演示下指标计算，如 rsi 上穿 30 可能表示短期价格有反弹，就可以发个报警，增加对这个标的的观察。\n指标计算可用 talib 实现，如下所示：\nimport talib data = history_bars(\u0026#34;000001\u0026#34;, length=100, equity_type=\u0026#34;stock\u0026#34;) rsi = talib.RSI(data[\u0026#39;close\u0026#39;].values) rsi0 = rsi[-1] rsi1 = rsi[-2] crossover = rsi0 \u0026gt; 30 and rsi1 \u0026lt; 30 if crossover: print(\u0026#34;触发报警条件\u0026#34;) 希望本文对您有所帮助。如果有任何问题或建议，欢迎随时联系我。\n","date":"2024-07-12","externalUrl":null,"permalink":"/posts/2024-07-12-download-quote-data-in-china-market/","section":"文章","summary":"本文介绍如何使用 akshare 封装一个易用的下载国内期货、期货指数、股票和股票指数的历史行情数据接口。\n目标接口的定义如下所示：\nhistory_bars(symbol, length=100, equity_type=\"future\") 这么封装也是为了便于使用。\n","title":"使用 Akshare 下载国内的期货（主力连续）、股票和指数的历史行情数据","type":"posts"},{"content":"在上一篇博文中，介绍了永续合约资金费率套利的原理。任何一个交易策略如果想要长期使用，必须挖掘和分析它在不同行情下的表现，而这一过程离不开历史数据的收集。\n本文将重点介绍如何收集资金费率的历史数据。\n交易所页面 # 资金费率是由每家交易所计算的，因此，最先想到的数据来源自然是交易所本身。\n早期，交易所只提供最新的资金费率，过期数据无法获取。然而，随着资金费率套利的增多，交易所开始提供全量或近几个月的资金费率数据供用户下载。\n如下图所示，Binance交易页实时计算的下一次资金费率：\n这个值并不是固定的，随着行情变化会有所波动。它只是预测下一次的资金费率，而非历史数据。\n那么如何获取历史数据呢？\n以Binance为例，它提供了一个可以查看和下载全量资金费率历史数据的页面，访问 Funding Rate History。\n如下图所示，只需点击右上角的 \u0026ldquo;Save as CSV\u0026rdquo; 即可下载所有数据：\n其他交易所的获取方式也基本一致，如Bybit和OKX，它们也提供了类似的历史资金费率页面。\nBybit - Funding Rate History，Bybit和Binance一样，提供全量数据下载。 OKX - Funding Rate History，OKX只提供近三个月的数据，无法下载全量数据，这是一个缺点。 对于数据分析而言，从页面下载已经足够。如果是实盘交易，则需要借助交易所的API，实时分析历史数据和获取下一次的资金费率。\n交易所的API接口 # 接下来介绍如何通过API获取资金费率。这需要你至少了解一种编程语言，如Python，并且能够调用交易所API。\n以下是三个主流交易所的资金费率API文档：\nBinance Coin-M - Funding Rate History API Binance USDⓈ-M - Funding Rate History API Bybit - Funding Rate History API OKX - Funding Rate History API Binance的正向合约和反向合约需要分别调用不同的API，这是对接时要注意的点。为了简化API对接，我将使用Python的第三方包ccxt来连接不同的交易所。\n安装（已有 Python 环境）：\npip install ccxt 只需通过ccxt提供的fetch_funding_rate_history方法调用上述API文档中的接口。以下是使用API从Binance下载资金费率数据，以永续合约BTCUSDT为例。\nimport ccxt binance = ccxt.binanceusdm() funding_rates = binance.fetch_funding_rate_history(symbol=\u0026#34;BTCUSDT\u0026#34;) print(funding_rates) 输出数据包含多个时间点的资金费率数组，如下所示：\n[ { \u0026#39;info\u0026#39;: {\u0026#39;symbol\u0026#39;: \u0026#39;BTCUSDT\u0026#39;, \u0026#39;fundingTime\u0026#39;: \u0026#39;1716624000000\u0026#39;, \u0026#39;fundingRate\u0026#39;: \u0026#39;0.00010000\u0026#39;, \u0026#39;markPrice\u0026#39;: \u0026#39;68775.00000000\u0026#39;}, \u0026#39;symbol\u0026#39;: \u0026#39;BTC/USDT:USDT\u0026#39;, \u0026#39;fundingRate\u0026#39;: 0.0001, \u0026#39;timestamp\u0026#39;: 1716624000000, \u0026#39;datetime\u0026#39;: \u0026#39;2024-05-25T08:00:00.000Z\u0026#39; }, { \u0026#39;info\u0026#39;: {\u0026#39;symbol\u0026#39;: \u0026#39;BTCUSDT\u0026#39;, \u0026#39;fundingTime\u0026#39;: \u0026#39;1716652800000\u0026#39;, \u0026#39;fundingRate\u0026#39;: \u0026#39;0.00010000\u0026#39;, \u0026#39;markPrice\u0026#39;: \u0026#39;68945.95530496\u0026#39;}, \u0026#39;symbol\u0026#39;: \u0026#39;BTC/USDT:USDT\u0026#39;, \u0026#39;fundingRate\u0026#39;: 0.0001, \u0026#39;timestamp\u0026#39;: 1716652800000, \u0026#39;datetime\u0026#39;: \u0026#39;2024-05-25T16:00:00.000Z\u0026#39; }, ... ] info 字段是API返回的原始信息，其他字段是ccxt整合不同交易所后生成的通用字段。\n仔细观察程序打印结果，你会发现API返回的并非全量数据。由于API每次返回的数据量有限，需要多次循环才能获取全量数据。\n我将这个过程封装为一个函数，代码如下：\ndef binance_fetch_all_funding_rate_history(symbol, limit=100): all_funding_rates = [] since = None end = None if not symbol.endswith(\u0026#34;USD\u0026#34;): binance = ccxt.binanceusdm() else: binance = ccxt.binancecoinm() while True: funding_rates = binance.fetch_funding_rate_history( symbol, since=since, limit=limit, params={\u0026#34;endTime\u0026#34;: end} ) if not funding_rates: break all_funding_rates = funding_rates + all_funding_rates oldest_timestamp = funding_rates[0][\u0026#34;timestamp\u0026#34;] end = oldest_timestamp - 1 since = oldest_timestamp - limit * 8 * 3600000 time.sleep(0.2) # 防止触发频率限制 return all_funding_rates 现在，只需调用binance_fetch_all_funding_rate_history函数获取数据并将其保存即可。\nimport pandas as pd def main(): funding_rates = binance_fetch_all_funding_rate_history(\u0026#34;BTCUSDT\u0026#34;) data = pd.DataFrame(funding_rates) data[[\u0026#34;datetime\u0026#34;, \u0026#34;fundingRate\u0026#34;]].to_csv(\u0026#34;binance_BTCUSDT.csv\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() 这里保存了两个重要字段：datetime和fundingRate，并将其保存为名为binance_BTCUSDT.csv的文件。\nBybit和OKX的实现代码可查看我的Github Gist - 资金费率下载。需要提醒的是，OKX无论是从页面还是通过API下载，都只能获得最近三个月的数据。\n在实盘场景下，查看交易所最近预测的资金费率，也可通过ccxt直接的方法获取。\nimport ccxt proxies = {} binance = ccxt.binanceusdm({\u0026#34;proxies\u0026#34;: proxies}) funding_rate = binance.fetch_funding_rate(symbol=\u0026#34;BTCUSDT\u0026#34;) print(funding_rate) 输出结果如下：\n{ \u0026#39;info\u0026#39;: {\u0026#39;symbol\u0026#39;: \u0026#39;BTCUSDT\u0026#39;, \u0026#39;markPrice\u0026#39;: \u0026#39;61979.80000000\u0026#39;, \u0026#39;indexPrice\u0026#39;: \u0026#39;61987.25595745\u0026#39;, \u0026#39;estimatedSettlePrice\u0026#39;: \u0026#39;61972.26180915\u0026#39;, \u0026#39;lastFundingRate\u0026#39;: \u0026#39;0.00010000\u0026#39;, \u0026#39;interestRate\u0026#39;: \u0026#39;0.00010000\u0026#39;, \u0026#39;nextFundingTime\u0026#39;: \u0026#39;1719792000000\u0026#39;, \u0026#39;time\u0026#39;: \u0026#39;1719776533000\u0026#39;}, \u0026#39;symbol\u0026#39;: \u0026#39;BTC/USDT:USDT\u0026#39;, \u0026#39;markPrice\u0026#39;: 61979.8, \u0026#39;indexPrice\u0026#39;: 61987.25595745, \u0026#39;interestRate\u0026#39;: 0.0001, \u0026#39;estimatedSettlePrice\u0026#39;: 61972.26180915, \u0026#39;timestamp\u0026#39;: 1719776533000, \u0026#39;datetime\u0026#39;: \u0026#39;2024-06-30T19:42:13.000Z\u0026#39;, \u0026#39;fundingRate\u0026#39;: 0.0001, \u0026#39;fundingTimestamp\u0026#39;: 1719792000000, \u0026#39;fundingDatetime\u0026#39;: \u0026#39;2024-07-01T00:00:00.000Z\u0026#39;, \u0026#39;nextFundingRate\u0026#39;: None, \u0026#39;nextFundingTimestamp\u0026#39;: None, \u0026#39;nextFundingDatetime\u0026#39;: None, \u0026#39;previousFundingRate\u0026#39;: None, \u0026#39;previousFundingTimestamp\u0026#39;: None, \u0026#39;previousFundingDatetime\u0026#39;: None } 有了这些基础数据，就可以进入下一步，分析资金费率的套利空间。\n第三方平台 # 如果你不想自己整理数据，也有一些三方平台，如Coinglass，提供了资金费率分析的工具，聚合了大部分主流交易所的数据，并提供了一些简单对比分析图表。\n访问地址：Funding Rate。\n如果你不需要深入分析甚至甚至是回测，这或许是一个不错的选择。\n","date":"2024-07-01","externalUrl":null,"permalink":"/posts/2024-06-27-collect-the-funding-rate-data/","section":"文章","summary":"在上一篇博文中，介绍了永续合约资金费率套利的原理。任何一个交易策略如果想要长期使用，必须挖掘和分析它在不同行情下的表现，而这一过程离不开历史数据的收集。\n","title":"永续合约资金费率数据搜集","type":"posts"},{"content":"在加密货币市场中，我基本围绕一个思路展开交易：通过套利控制回撤，震荡行情赚些许收益，耐心等待益趋势追踪策略的高收益。于是，合适的低风险套利策略就变得很重要，这决定了我等待大趋势的耐心程度。\n本文分享一个套利策略：资金费率套利。\n资金费率套利是一种基于加密货币市场特有的永续合约（Perpetual Contract）来实现的巧妙赚取收益的策略。\n本文将介绍什么是资金费率、它的影响因素、基于它的套利思路、一些简单的操作方法等。\n注：去年我用一个账号测试了现货合约对冲的费率套利策略，大概有 35% 的收益，主因还是现在是币圈的牛市行情，资金费率也相对较高。\n什么是资金费率? # 资金费率是一种存在于币圈永续合约中的独特机制。\n永续合约是一种没有到期日期的衍生品合约，最早是由 BitMEX 推出。由于永续合约不同于交割合约，没有到期日按现货价格交割的机制，为了保证它的价格与现货价格保持一致，交易所引入了资金费率机制。它的作用是平衡市场上的买卖力量。\n简单化理解资金费用：当市场上买单多于卖单时，多头需要支付资金费率给空头，反之亦然。资金费率的高低取决于市场需求和供给的关系。\n实际运作时，每隔一段时间（通常是每 8 小时），交易所会计算出一个资金费率，并在多空之间转移资金费。\n举例来说，假设你在某交易所开设了一个 10 个比特币（BTC）的多头永续合约仓位，如果当前资金费率为 0.05%，则下一个 8 小时结算时，你需要支付 10 BTC * 0.05% = 0.005 BTC 的资金费用给空方。\n这个机制就是我们实施套利的基本前提。\n注意，资金费用 = 仓位价值 * 资金费率，而仓位价值一般是基于 Mark Price 计算的，本文重点在资金费率，计算资金费用时会假设 mark price 不变，忽略价格的影响。\n资金费率的计算因子 # 在正式介绍资金费率套利策略前，先说下资金费率的影响因子。\n不同交易所资金费率在公式上或有些许差异，但主要两个影响因素：无风险利率和溢价指数。\n无风险利率：无风险利率是指在不承担任何风险的情况下可以获得的收益率，通常基于国家发行的短期债券利率。在加密货币交易所中，或是主要与数字货币的借贷利率相关。\n溢价指数：溢价指数反映了永续合约价格与现货价格之间的差异。当永续合约价格高于现货价格时，溢价为正，反之亦然。溢价指数直接影响资金费率，因为它反映了市场参与者的预期。\n某种意义上，资金费率套利就是通过捕捉无风险利率和期现价差来实现收益的一种方式。这看起来似乎有点是同时把质押借贷和期现套利的钱赚了。\n如果对公式有兴趣，我常用的三个交易所资金费率计算公式文档如下：\nBybit - Introduction to Funding Rate； OKX - Funding fee Mechanism Binance - Introduction to Binance Futures Funding Rates 基于资金费率的套利思路 # 什么是资金费率套利策略呢？\n前面的案例，空方虽然赚了 0.005 BTC 的资金费，但它的收益受到 BTCUSDT 价格变动的影响，如果跌了还好，不但赚了资金费用，还赚了价格变动的收益。但问题是，这是不确定的，如果涨了，我们就亏了，而且价格的波动是远大于资金费率的收益的。\n如何消除这个不确定性呢？\n我们可以在做空合约的同时，再买入等量的现货，这样现货与合约间就相互抵消价格变动带来的不确定性。这就是一种资金费率套利策略。\n这其实就是一种 delta 中性策略。所谓 delta 中性，即它的盈亏与市场价格无关。这个策略的实现一般有两种，分别是现货合约对冲与跨交易所对冲。\n现货合约对冲 # 现货合约对冲策略指在现货和永续合约市场同时持有相等数量的多头和空头仓位锁定资金费率收益。\n操作：\n在现货市场购买一定数量的加密货币。 同时，在永续合约市场开设等量的空头仓位。 持仓期间，根据资金费率收取或支付费用。 持仓结束时，同时平仓现货和合约仓位，结算收益。 案例：\n假设我在现货市场购买了 10 个 BTC，并开了价值 10 BTC 的 BTCUSDT 永续合约空头。如果当前资金费率为 0.01%，我将定期收到资金费率费用。如资金费率为0.01%，每 8 小时将收到0.001 个 BTC 的资金费率费用。\n现货合约对冲是较简单的资金费率套利策略，如果频繁操作，实际的收益还要考虑交易手续费和滑点。\n如果资金费率较为稳定（如 BTCUSDT 永续合约，资金费率大部分时间大于 0），这策略就可近似理解为一种无风险套利策略了，一次操作，长期持有。\n如果要最大化收益，可考虑短期交易一些资金费率较高的永续合约，这时我们就要考虑收益是否能覆盖换仓的成本（即交易费和滑点）加上不换仓的收益。当然，换仓后的收益不仅仅和当前的资金费率有关，还和资金费率的稳定性有关，即是不是只赚了一两次的高收益。\n这个策略的重点是基于对品种的把控，必须是长期上涨的品种，如 BTC。我的策略就是长期有稳定资金费率的合约。\n我收集了 binance 历史上的资金费率数据并简单绘制了它的 BTCUSDT 资金费率曲线图，如下所示。\n注意大部分情况只都位于 0 以上，即红色虚线以上。我还统计下了它在资金费率历史上 \u0026gt; 0 比率，大概是 87.36%，而 bybit 大概是 86.37%。\n还有，对于现货合约对冲套利，可以在支持统一账户中的交易所中进行，这能最大化资金利用率，免去不必要的借贷。我提到的 Bybit、OKX 和 Binance 都是支持统一保证金账户的。\n跨交易所对冲 # 跨交易所对冲策略是指利用不同交易所之间的资金费率差异来进行套利，以捕捉资金费率之间的收益差异。\n操作：\n在资金费率较高的交易所开设空头仓位。 在资金费率较低的交易所开设多头仓位。 定期监控两个交易所的资金费率差异。 当资金费率差异达到预期收益时，平仓并结算收益。 案例：\n假设当前交易所 A 资金费率为 0.02%，交易所 B 资金费率为 -0.01%。\n我可以在交易所 A 开价值 1 BTC 的 BTCUSDT 永续合约空头仓位，在交易所 B 开等量多头仓位。下个 8 小时，我将在交易所 A 收入 0.02% 的资金费用，但在交易所 B 收取 0.01% 的资金费率。\n这样我就可以赚取两者之间的资金费率差异，获得它们的差额收益：1 * 0.02% + 1 * 0.01% = 0.0003 BTC。\n滑点和交易费用的影响 # 在资金费率套利时，滑点和交易费用是影响收益的重要因素。\n交易滑点，指由于市场流动性不足或订单量较大导致的实际成交价与预期价间的差异。 交易费用，即买卖的手续费，如果能成为交易所的 VIP 等级，这块会有手续费减免。 为了最大化资金费率套利的收益，建议采取一些优化措施：\n选择流动性高的交易所和交易对，以减少滑点。 定期监控和调整仓位，以应对市场变化。 优化交易策略，减少交易频率和交易成本。 如果发现某交易所的流动性较差，导致买入现货时出现较大的滑点，就不是理想选择。导致滑点的原因除了交易所本身的流动性，还和订单量有关，如果量足够大，同样有滑点严重的问题，可分批交易以减少单次交易量，从而减少滑点。\n总结 # 资金费率套利是一种通过捕捉市场资金费率和价格差异实现收益的套利策略。\n通过现货合约对冲和跨交易所对冲等方法，可以在不承担市场价格波动风险的情况下获得稳定收益。但要成功执行这个套利策略，滑点和交易费用是要重点考虑的。\n最后，希望本文能帮助大家更好地理解和应用资金费率套利策略。\n本文内容仅代表作者个人观点，仅供学习与交流使用，不构成任何投资建议或交易指导。投资者应根据自身的财务状况和风险承受能力，自行研究识别并评估潜在风险。\n","date":"2024-06-23","externalUrl":null,"permalink":"/posts/2024-06-23-funding-fee-arbitrage-in-cryptocurrency-market/","section":"文章","summary":"在加密货币市场中，我基本围绕一个思路展开交易：通过套利控制回撤，震荡行情赚些许收益，耐心等待益趋势追踪策略的高收益。于是，合适的低风险套利策略就变得很重要，这决定了我等待大趋势的耐心程度。\n","title":"谈谈加密货币市场上的资金费率套利","type":"posts"},{"content":"今天这篇可能有点跨域了，其实这段时间，我一直在做交易。如果大家对自动化交易感兴趣，我也有很多内容可以写。\n这两天发现一个可在 Python 显示交易图表的库，名为 lightweight-charts-python。顾名思义，它是基于 tradingview 轻量级库 lightweight-charts 开发而来。\nTradingView 是一款非常流行的交易行情分析软件，而 lightweight-charts 是它提供的精简版 Js 开源库。因为 TradingView 本身就是一款专注交易的软件，lightweight-charts 有这高性能和用户体验友好的特点。\n它的实现是 Python 通过 webview 集成 lightweight-charts 开发而来的库，使用 Python 的小伙伴无需学习 Javascript 也能使用 lightweight-charts 了。\n通过 Python 实现，还有个优势，容易与其他框架结合，如国内非常流行的实盘框架 vnpy，它的 GUI 界面就是用 pyside6 实现，这个库非常容易集成到 pyside6 中，从而集成到 vnpy。\nOK，让我们正式开始演示这个库的使用吧。\n安装命令 # pip install lightweight-charts 行情数据 # 为了体验 lightweight-charts-python 库的使用，提前准备行情数据必要的步骤。\nlightweight-charts-python 的 GitHub 仓库中提供了一些样例数据，我们也可以从一些交易软件下载。不过，既然都来看这篇文章了，肯定都是会编程的，我推荐开源库获取数据。\n我将用 tushare 下载行情数据，或者也可以用 akshare 等一些开源库。假设下载代码为 000001.SZ 的平安银行。\nimport tushare as ts pro = ts.pro_api() code = \u0026#34;000001.SZ\u0026#34; df = pro.query(\u0026#34;daily\u0026#34;, ts_code=code, start_date=\u0026#34;20180701\u0026#34;, end_date=\u0026#34;20240510\u0026#34;) 通过 pandas 将其整理为满足 lightweight-charts 目标的数据格式，即包含 date、open、high、low、close、volume 字段，还要保证 date 为日期格式，且数据按日期升序排列。\ndf[\u0026#34;date\u0026#34;] = pd.to_datetime(df[\u0026#34;trade_date\u0026#34;]) df = df.sort_values(by=\u0026#34;date\u0026#34;, ascending=True) df.rename(columns={\u0026#34;vol\u0026#34;: \u0026#34;volume\u0026#34;}, inplace=True) df = df[[\u0026#34;date\u0026#34;, \u0026#34;open\u0026#34;, \u0026#34;high\u0026#34;, \u0026#34;low\u0026#34;, \u0026#34;close\u0026#34;, \u0026#34;volume\u0026#34;]] df.to_csv(f\u0026#34;D_{code}.csv\u0026#34;) 目录就有了名为 D_000001.SZ.csv 的行情数据文件。依次类推，还可下载周线和月线。分钟线数据好像需要更高积分才能下载。\n快速上手 # 首先，体验下它最基础使用方法，显示历史 K 线图表。\nimport time import pandas as pd from lightweight_charts import Chart def main(): chart = Chart() df = pd.read_csv(\u0026#34;D_000001.SZ.csv\u0026#34;) chart.set(df) chart.show(block=True) if __name__ == \u0026#34;__main__\u0026#34;: main() 如上的代码中，Chart 是它的核心类，通过 pandas 的读取 csv 文件数据并将其 DataFrame 传递给 chart 即可完成历史行情的设置。\n运行代码，你将能看到如下的效果：\n新增 Bar/Tick # 如要实时的行情，我们可通过 Chart 提供的方法更新图表。它提供了两个方法，分别是 update 可用于更新 Bar 蜡烛图和 update_from_tick 从 tick 数据更新图表。\n首先，演示新增 bar 更新图表。我们将之前准备的数据截断，df.iloc[-10:] 保留 10 个 bar 为演示数据。\nchart = Chart() df = pd.read_csv(\u0026#34;D_000001.SZ.csv\u0026#34;) chart.set(df.iloc[:-10]) chart.show() for i, bar in df.iloc[-10:].iterrows(): chart.update(bar) time.sleep(1) 每隔 1 秒，新增 1 个 bar。动图效果如下：\n实际场景下，实时更新 tick 才是我们的目标。只要将实时 tick 不断投喂给 chart 即可。我暂时没有接入实时数据，将用随机模拟的方式生成后续数据。\nchart = Chart() df = pd.read_csv(\u0026#34;D_000001.SZ.csv\u0026#34;, parse_dates=[\u0026#34;date\u0026#34;]) chart.set(df) chart.show() last_time = df.iloc[-1][\u0026#34;date\u0026#34;] + datetime.timedelta(days=1) last_price = df.iloc[-1][\u0026#34;close\u0026#34;] while True: time.sleep(0.1) change_percent = 0.002 change = last_price * random.uniform(-change_percent, change_percent) new_price = last_price + change new_time = pd.to_datetime(last_time) + datetime.timedelta(hours=1) tick = pd.Series({\u0026#34;time\u0026#34;: new_time, \u0026#34;price\u0026#34;: new_price}) chart.update_from_tick(tick) last_price = new_price last_time = new_time 随机数据基于最新的价格和时间生成，重点就是其中这一段代码：\nchange_percent = 0.002 change = last_price * random.uniform(-change_percent, change_percent) new_price = last_price + change new_time = pd.to_datetime(last_time) + datetime.timedelta(hours=1) 将其组织成 lightweight-charts 要求的 tick 数据格式传递给 chart 即可。\ntick = pd.Series({\u0026#34;time\u0026#34;: new_time, \u0026#34;price\u0026#34;: new_price}) chart.update_from_tick(tick) 动图效果如下：\n指标创建 # 指标 Indicator 是交易图表不可缺少的部分，用 lightweight-charts 要实现指标只要通过 chart 的方法 create_line 创建一条 line，设置 line 数据即可。如果你的指标不是线条，而是直方图，Chart 也提供了 create_histogram。\n我就以简单移动平均作为演示吧。指标计算函数如下：\ndef sma(df, period: int = 50): return pd.DataFrame( {\u0026#34;time\u0026#34;: df[\u0026#34;date\u0026#34;], f\u0026#34;SMA {period}\u0026#34;: df[\u0026#34;close\u0026#34;].rolling(window=period).mean()} ).dropna() 从如上的计算函数可知，提供给 line 的数据要求包含时间和指标值，而指标值的列名要和 Line 的名称相同。主函数中新增两行代码引入指标 Line：\n# 保持与 sma 返回的 SMA {period} 相同 line = chart.create_line(\u0026#34;SMA 30\u0026#34;) line.set(sma(df, period=30)) 效果如下：\n如果要实时更新的话，计算后续数值并通过 line.update 方法更新上去即可。\nSMA 均线还是主图指标，如果希望在副图上显示指标，可通过 chart.create_subchart 创建副图，在其中显示副图指标。\n核心代码如下所示：\nchart = Chart(inner_height=0.7) chart.time_scale(visible=False) # 将主图的时间轴隐藏 # 获取数据省略 # sync 参数保持与主图同步 rsi_chart = chart.create_subchart(height=0.3, width=1, sync=True) rsi_line = rsi_chart.create_line(\u0026#34;RSI\u0026#34;) rsi_line.set(rsi(df)) 如下 rsi 计算函数部分，其中的 RSI 指标用的是 talib 实现。\ndef rsi(df, period: int = 14): return pd.DataFrame( {\u0026#34;time\u0026#34;: df[\u0026#34;date\u0026#34;], \u0026#34;RSI\u0026#34;: talib.RSI(df[\u0026#34;close\u0026#34;], timeperiod=14)} ) 效果如下：\n启用图例 # 我们已经添加了指标，但只看到线条，如想查看实际数值，通过 chart.legend(True) 启用图例即可。\nchart.legend(True) # 主图 rsi_chart.legend(True) # 副图 效果如下：\n多图表支持 # 前面通过 create_subchart 创建副图来展示副图指标，其实我们还可以基于它创建多图表。交易员可能要在不同的图表上展示不同周期或不同资产的行情，以便于寻找交易机会。\n我将以在不同图表展示统一个资产的日线、周线和月线为例，演示它的使用。\nchart = Chart(inner_width=0.5, inner_height=0.5) weekly_chart = chart.create_subchart(width=0.5, height=0.5, sync=True) monthly_chart = chart.create_subchart(width=1, height=0.5, sync=True) df = pd.read_csv(\u0026#34;D_000001.SZ.csv\u0026#34;) weekly_df = pd.read_csv(\u0026#34;W_000001.SZ.csv\u0026#34;) monthly_df = pd.read_csv(\u0026#34;M_000001.SZ.csv\u0026#34;) chart.set(df) weekly_chart.set(weekly_df) monthly_chart.set(monthly_df) chart.show(block=True) 如上代码创建了三个周期的 chart，分别展示日线、周线和月线。其中的周线和月线图表都是基于日线 chart 创建的。它默认是按从左到右从上到下排列的，创建的时候注意设置 chart 的宽高即可。\n最终效果如下：\n我在测试时发现它的同步有点问题，好像只能在父子间同步,即在日线可以同步周线和月线，在周线或月线可以同步日线，但不能保持周线和月线的同步。或许要看 JS 库才能解决了吧?\n其他更多 # 更多内容就不一一展开了，如果大家有兴趣的话，可留言说明，我可以继续完成其他部分，如周期切换、按 symbol 搜索，指标选择自定义等等，甚至与实盘框架集成，实盘下单，如 vnpy 实盘框架，vnpy 作者自研了一个图表，但功能有限，lightweight-charts 也可直接与 PySide6 集成，集成到 vnpy 框架中。\n有了这个库，我们可以做一些自己想要的能力，如手动回放回测、或者自动策略与手动相结合的回测，一切尽在掌握。毕竟，自己编程，才能丰衣足食。\n","date":"2024-05-13","externalUrl":null,"permalink":"/posts/2024-05-10-lightweight-charts-python/","section":"文章","summary":"今天这篇可能有点跨域了，其实这段时间，我一直在做交易。如果大家对自动化交易感兴趣，我也有很多内容可以写。\n这两天发现一个可在 Python 显示交易图表的库，名为 lightweight-charts-python。顾名思义，它是基于 tradingview 轻量级库 lightweight-charts 开发而来。\n","title":"一个 Python 轻量级交易图表库 - lightweight-charts-python","type":"posts"},{"content":"和其他语言相比，Go 中有相同也有不同，相同的是实现思路上和其他语言没啥差异，不同在于 Go 采用的是 goroutine + channel 的并发模型，与传统的进程线程相比，实现细节上存在差异。\n本文将从实际场景和它的一般实现方式展开，逐步讨论这个话题。\n简介 # 什么是优雅停止？在谈优雅停止前，我们可以说说什么是优雅重启，或者说热重启。\n简言之，优雅重启就是在服务升级、配置更新时，要重新启动服务，优雅重启就是在服务不中断或连接不丢失的情况下，重启服务。优雅重启的整个流程中，新的进程将在旧的进程停止前启动，旧进程会完成活动中的请求后优雅地关闭进程。\n优雅重启是服务开发中一个非常重要的概念，它让我们在不中断服务的情况下，更新代码和修复问题。它在维持高可用性的生产环境中尤其关键。\n从上面的这段可知，优雅重启是由两个部分组成，分别是优雅停止和启动。\n本文重点介绍优雅停止，而优雅启动的整个流程要借助于外部工具控制，如 k8s 的容器编排。\n优雅停止 # 优雅停止，即要在停止服务的同时，保证业务的完整性。从目标上看，优雅停止经历三个步骤：通知服务停止、服务启动清理，等待清理确认退出。\n要停止一个服务，首先是通过一些机制告知服务要执行退出前的工作，最常见的就是基于操作系统信号，我们惯例监听的信号主要是两个，分别是由 kill PID 发出的 SIGTERM 和 CTRL+C 发出的 SIGINT。 其他信号还有，CTRL+/ 发出的 SIGQUIT。\n当接收到指定信号，服务就要停止接受新的请求，且等待当前活动中的请求全部完成后再完全停止服务。\n接下来，开始具体的代码实现部分吧。\n从 HTTP 服务开始 # 谈优雅重启，最常被引用的案例就是 HTTP 服务，我将通过代码逐步演示这个过程。如下是一个常规 HTTP 服务：\nfunc hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Hello World\\n\u0026#34;) } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, hello) log.Println(\u0026#34;Starting server on :8080\u0026#34;) if err := http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil); err != nil { log.Fatal(\u0026#34;ListenAndServe: \u0026#34;, err) } } 我们通过 time.Sleep 增加 hello 的耗时，以便于调试。\nfunc hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Hello World\\n\u0026#34;) time.Sleep(10 * time.Second) } 运行：\n$ go run main.go 通过 curl 请求访问 http://localhost:8080/ ，它进入到 10 秒的处理阶段。假设这时，我们 CTRL+C 请求退出，HTTP 服务会直接退出，我们的 curl 请求被直接中断。\n我们可以使用 Go 标准库提供的 http.Server 有一个 Shutdown 方法，可以安全地关闭服务器而不中断任何活动的连接。而我们要做的，只需在收到停止信号后，执行 Shutdown 即可。\n信号方面，我们通过 Go 标准库 signal 实现，它提供了一个 Notify 函数，可与 chan nnel 配合传递信号消息。我们监听的目标信号是 SIGINT 和 SIGTERM。\n重新修改 HTTP 服务入口，使用 http.Server 的 Shutdown 函数关闭 Server。\nfunc main() { mux := http.NewServeMux() mux.HandleFunc(\u0026#34;/\u0026#34;, hello) server := http.Server{Addr: \u0026#34;:8080\u0026#34;, Handler: mux} go server.ListenAndServe() quit := make(chan os.Signal, 1) // 注册接收信号的 channel signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) \u0026lt;-quit // 等待停止信号 if err := server.Shutdown(context.Background()); err != nil { log.Fatal(\u0026#34;Shutdown: \u0026#34;, err) } } 我们将 server.ListenAndServe 运行于另一个 goroutine 中同时忽略了它的返回错误。\n通过 signal.Notify 注册信号。当收到如 CTRL+C 或 kill PID 发出的中断信号，执行 serve.Shutdown，它会通知到 server 停止接收新的请求，并等待活动中的连接处理完成。\n现在运行 go run main.go 启动服务，执行 curl 命令测试接口，在请求还没有返回之时，我们可以通过 CTRL+C 停止服务，它会有一段时间等待，我们可以在这个过程中尝试 curl 请求，看它是否还接收新的请求。\n如果希望防止程序假死，或者其他问题导致服务长时间无法退出，可通过 context.WithTimeout 方法包装下传递给 Shutdown 方法的 ctx 变量。\nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatal(\u0026#34;Shutdown: \u0026#34;, err) } 到这里，我们就介绍完了 Go 标准库 net/http 的优雅停止的使用方案。\n抽象出一个常规方案 # 如果开发一个非 HTTP 的服务，如何让它支持优雅停止呢？毕竟不是所有项目都是 HTTP 服务，不是所有项目都有现成的框架。\n本文开头提到的的三步骤，net/http 包的 Shutdown 把最核心的服务停止前的清理和等待都已经在内部实现了。我们可解读下它的实现。\n进入到 Shutdown 的源码中，重点是开头的第一句代码，如下所示：\n// future calls to methods such as Serve will return ErrServerClosed. func (srv *Server) Shutdown(ctx context.Context) error { srv.inShutdown.Store(true) // ...其他清理代码 // ...等待活动请求完成并将其关闭 } inShutdown 是一个标志位，用于标识程序是否已停止。为了解决并发数据竞争，它的底层类型是 atomic.bool，。\n在 server.go 中的 Server.Serve 方法中，通过判断 inShutdown 决定是否继续接受新的请求。\nfunc (srv *Server) Serve(l net.Listener) error { // ... for { rw, err := l.Accept() if err != nil { if srv.shuttingDown() { return ErrServerClosed } // ... } 我们可以从如上的分析中得知，要让 HTTP 服务支持优雅停止要启动两个 goroutine，Shutdown 运行与 main goroutine 中，当接收中停止信号，通过 inShutdown 标志位通知运行中的 goroutine。\n用简化的代码表示这个一般模式。\nvar inShutdown bool func Start() { for !inShutdown { // running time.Sleep(10 * time.Second) } } func Shutdown() { inShutdown = true } func main() { go Start() quit = make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) \u0026lt;- quit Shutdown() } 大概看起来是那么回事，但这里的代码少了一个步骤，即 Shutdown 没有等待 Start 完成。\n标准库 net/http 是通过 for 循环不断检查是否有活动中的连接，如果连接没有进行中请求会将其关闭，直到将所有连接关闭，便会退出 Shutdown。\n核心代码如下：\nfunc (srv *Server) Shutdown(ctx context.Context) { // ...之前的代码 timer := time.NewTimer(nextPollInterval()) defer timer.Stop() for { if srv.closeIdleConns() { return lnerr } select { case \u0026lt;-ctx.Done(): return ctx.Err() case \u0026lt;-timer.C: timer.Reset(nextPollInterval()) } } } 重点就是那句 closeIdleConns，它负责检查是否还有执行中的请求。我就不把这部分的源代码贴出来了。而检查频率是通过 timer 控制的。\n现在让简化版等待 Start 完成后才退出。我们引入一个名为 isStop 的标志位以监控停止状态。\nvar inShutdown bool var isStop bool func Start() { for !inShutdown { // running time.Sleep(10 * time.Second) } isStop = true } func Shutdown() { inShutdown = true timer := time.NewTimer(time.Millisecond) defer timer.Stop() for { if isStop { return } \u0026lt;- timer.C timer.Reset(time.Millisecond)) } } 如上的代码中，Start 函数退出时会执行 isStop = true 表明已退出，在 Shutdown 中，通过定期检查 isStop 等待 Start 退出完成。\n此外，net/http 的 Shutdown 方法还接收了一个 context.Context 参数，允许实现超时控制，从而防止程序假死或强制关闭。\n需要特别指出的是，示例中用的 isStop 和 inShutdown 标志位为非原子类型，在正式场景中，为避免数据竞争，要使用原子操作或其他同步机制。\n除了用共享内存标志位在不同进程间传递状态，也可以通过 channel 实现，或你看到过类似如下的形式。\nvar inShutdown bool func Start(stop chan struct{}) { for !inShutdown { // running time.Sleep(10 * time.Second) } stop \u0026lt;- struct{}{} } func Shutdown() { inShutdown = true } func main() { stop := make(chan struct{}) defer close(stop) go Start(stop) go func() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) \u0026lt;-quit Shutdown() }() \u0026lt;-stop } 如上的代码中，Start 通过 channel 通知主 goroutine，当触发停止信号，isShutdown 通知 Start 要停止退出，它成功退出后，通过 stop \u0026lt;- struct{} 通知主函数，结束等待。\n总的来说，channel 的优势很明显，避免了单独管理一个 isStop 标志位来标识服务状态，并且免去了基于定时器的定期轮询检查的过程，还更加实时和高效。当然，net/http 使用轮询检查机制，是它的场景所决定，和我们这里不完全一样。\n一点思考 # Go 语言支持多种方式在 Goroutine 间传递信息，这催生了多样的优雅停止实现方式。如果是在涉及多个嵌套 Goroutine 的场景中，我们可以引入 context 来实现多层级的状态和信息传递，确保操作的连贯性和安全性。\n然尽管实现方式众多，但其核心思路是一致的，而底层目标始终是我们要保证处理逻辑的完整性。\n另外，通过将优雅停止与容器编排技术结合，并为服务添加健康检查，我们能够确保服务总有实例在活跃状态，实现真正意义上的优雅重启。这不仅提高了服务的可靠性，也优化了资源的利用效率。\n总结 # 本文探索了 Go 语言中优雅重启的实现方法，展示了如何通过 http.Server 的 Shutdown 方法安全地重启服务，以及使用 context 控制超时。基于此，我们抽象出了一般服务优雅停止的核心思路。\n最后，希望本文对你有所帮助，感谢关注我的公众号。\n","date":"2024-04-14","externalUrl":null,"permalink":"/posts/2024-04-14-graceful-stop-server-in-go/","section":"文章","summary":"和其他语言相比，Go 中有相同也有不同，相同的是实现思路上和其他语言没啥差异，不同在于 Go 采用的是 goroutine + channel 的并发模型，与传统的进程线程相比，实现细节上存在差异。\n","title":"基于 net/http 抽象出 go 服务优雅停止的一般思路","type":"posts"},{"content":"作为程序员，如果你要快速调试 HTTP 接口，首先想到的工具是什么？GUI 版 HTTP 客户端 Postman？命令行 curl？还是使用浏览器？不知道大家是否抱怨过，如 curl 这样的传统命令基本都存在一个问题：虽然功能强大，但用户体验极差，基本不考虑使用者的感受。\n本文介绍一款意外丢失 54k star，不到两年重获 31k 的 HTTP 客户端命令 - HTTPie ，它比 curl 更易于使用，是一款注重用户体验的 HTTP 客户端命令。\n一个趣事 # 先说关于 HTTPie 的意外丢失 star 数的一个趣事：\n2022 年，由于 HTTPie 的作者出现了一次误操作，将仓位设为私有，导致 HTTPie 的 star 直接清零。经历了两年时间，如今重新恢复到 36k star。\n当时，HTTPie 官方还写了一篇博客，专门反思了这次的事故，查看博客：How We Lost 54k stars。\n为什么推荐 HTTPie # HTTPie 的哲学是使 API 测试和调试变得直观友好，相比传统命令行工具（如 curl 和 wget），它提供了更丰富的功能和更易读的输出。\n来自官方的示例：\n首先是，它的命令行语法直观易懂，如发送 GET 请求只需 http GET \u0026lt;URL\u0026gt;，而 POST 请求则是 http POST \u0026lt;URL\u0026gt; \u0026lt;body\u0026gt;，这比 curl 的命令行参数更易于理解和记忆。\n它默认支持 JSON，它会将 HTTP 请求的 Content-Type 设置为 application/json 并自动将命令行数据参数序列化为 JSON，这使得很轻松即可与 JSON 主导的 RESTful API 交互。\n它默认支持输出的格式化和着色，这使得响应易于阅读和分析。成功的响应、重定向和错误都会以不同的颜色高亮显示。它还有其他高级输出选项，允许以多种格式查看响应，包括原始 HTTP 数据和仅正文。它支持输出重定向到文件或其他程序，为处理响应数据提供了灵活性。\n它允许创建会话，这意味着用户可以在多个请求之间保持某些参数不变（如认证令牌），从而简化了需要认证的 API 交互。\n其它还有如它提供了丰富的插件和扩展的支持，得到了开源社区的支持，一个 HTTP 客户端工具的 star 数足有 54k 之多（曾经）。\nOK，让我们结束废话，开始它的使用演示吧。\n安装 # 如下是 MacOS 的安装命令：\nbrew install httpie 成功安装 HTTPie 后，我们将有两个命令可供使用，分别是 http 和 https。注意，用于发起 HTTP 请求的命令不是 httpie，而它提供的另外两个命令，分别是 http 和 https，而 httpie 则是用于管理自身的扩展和插件。\n使用 # 我们先介绍最简单的使用案例，快速发起 GET 和 POST 请求。\nHTTPie 发起 GET 请求：\nhttp GET http://httpbin.org/get name==poloxue age==18 HTTPie 发起 POST 请求：\nhttp POST http://httpbin.org/post name=poloxue age=18 http/https 命令的完整语法，如下所示：\nhttp/https [flags] [METHOD] URL [ITEM [ITEM]] 易用性设计 # 除了 HTTPie 的命令语法设计，为提高 HTTPie 的易用性，作者在一些细节上也做了一些设计，如我们发起一个 HTTP 请求，拷贝 url 到终端，可直接通过在 scheme 后新增空格，回车即可：\n例如，GET 方法请求 http://httpbin.org/get，如下所示：\nhttp ://httpbin.org/get 演示效果：\n进一步，其中的 METHOD 默认也可省略，具体规则是：\n当有 data：POST，如 http ://httpbin.org/post name=poloxue age=18 当无 data：GET，如 http ://httpbin.org/get HTTPie 还支持一种 offline 模式，它是 debug 神器，只打印 HTTP 请求文本，但不真正发出网络请求。\nhttp --offline ://httpbin.org/get 输出内容：\nGET /get HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Connection: keep-alive Host: httpbin.org User-Agent: HTTPie/3.2.2 现在，让我们使用 offline 模式快速了解 HTTPie 中的 header 设置，query 参数 还有 JSON 请求体等是如何使用的吧。\n它们的设置规则，如下所示：\nheader：使用 key:value 设置 header； query string，使用 key==value，表示 query params，快速拼接 query string； body json data，使用 key=value 即可，更复杂结构，可通过命令管道或文件导入； 一个命令演示 Header、查询参数 和 JSON 请求的设置方法。\nhttp --offline POST http://httpbin.org/post \\ X-API-Token:123456\\ User-Agent:foo\\ name==poloxue\\ age==18\\ password=123456\\ email=poloxue123@gmail.com 通过 --offline 启用 offline 调试模式调试输出结果：\nPOST /post?name=poloxue\u0026amp;age=18 HTTP/1.1 Accept: application/json, */*;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Content-Length: 55 Content-Type: application/json Host: httpbin.org User-Agent: foo X-API-Token: 123456 { \u0026#34;email\u0026#34;: \u0026#34;poloxue123@gmail.com\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34; } 输出美化 # 开始说到，HTTPie 还有一个能力，它默认格式化和高亮响应内容，如此的话，我们就不需要在去找其他的 json 格式化命令了。\n默认效果如下： 我们可通过 --style 选项它的配色风格。\nhttp --style autumn GET ://httpbin.org/get 如果想查看所有配色，输入 http --style 查看：\nusage: http -s/--style {abap, algol, algol_nu, arduino, auto, autumn, borland, bw, colorful, default, dracula, emacs, friendly, friendly_grayscale, fruity, github-dark, gruvbox-dark, gruvbox-light, igor, inkpot, lightbulb, lilypond, lovelace, manni, material, monokai, murphy, native, nord, nord-darker, one-dark, paraiso-dark, paraiso-light, pastie, perldoc, pie, pie-dark, pie-light, rainbow_dash, rrt, sas, solarized, solarized-dark, solarized-light, staroffice, stata, stata-dark, stata-light, tango, trac, vim, vs, xcode, zenburn} [METHOD] URL [REQUEST_ITEM ...] error: argument --style/-s: expected one argument for more information: run \u0026#39;http --help\u0026#39; or visit https://httpie.io/docs/cli HTTPie 的格式方式也是可配置的，选项 --pretty，支持配置项有 all|colors|format|none。\nall，默认值，即高亮又格式化； format，不高亮但格式化； colors，高亮但不格式化； none，即禁用高亮和格式化； http --style=autumn ://httpbin.org/get 文件下载 # 除了这些常见的功能，下载文件也可通过 HTTPie 实现，一个选项 --donwload 即可。它还支持锻炼续传。\n使用命令：\nhttp --download https://github.com/httpie/cli/archive/master.tar.gz 也可指定文件下载路径，只要通过 --output/-o 选项指定输出的文件名称即可。\nhttp --donwload -o httpie.tar.gz https://github.com/httpie/cli/archive/master.tar.gz 断点续传的话，一个选项 --continue/-c 即可启用。\n演示效果，我选择下载一个大文件，飞书的海外版 Mac 安装包，。\nhttp -dco lark.dmg https://sf16-va.larksuitecdn.com/obj/lark-artifact-storage/49f5b75a/Lark-darwin_x64-6.11.16-signed.dmg HTTPie 就介绍这么多，其他还有 cookie、session，authentication 等请自行查阅文档 httpie 文档。\n总结 # 总体而言，HTTPie 是一个即强大又好用的 HTTPie 客户端，值得收藏到你的日常开发调试箱中。\n最后，感谢阅读，希望本文对你所有帮助，能进一步提升你在终端上的效率。\n","date":"2024-04-10","externalUrl":null,"permalink":"/posts/2024-04-10-httpie/","section":"文章","summary":"作为程序员，如果你要快速调试 HTTP 接口，首先想到的工具是什么？GUI 版 HTTP 客户端 Postman？命令行 curl？还是使用浏览器？不知道大家是否抱怨过，如 curl 这样的传统命令基本都存在一个问题：虽然功能强大，但用户体验极差，基本不考虑使用者的感受。\n","title":"HTTPie，一款意外丢失 54k star，不到两年重获 31k 的 HTTP 客户端命令","type":"posts"},{"content":"昨天发了一篇名为 \u0026ldquo;entr 一个通用的热重启方案\u0026rdquo; 的文章，写完这个命令的简单使用后，我开始思考一个问题：Go 这样的静态编译型语言是否能实现热更新？如果能，该如何实现呢？\n什么是热更新？ # 先简单说下什么是热更新。\n热更新，或称热重载或动态更新，是一种软件更新技术，允许程序在运行时，不停机更新代码或资源。这种技术特别适用于需要高可用性的场景，如线上服务和游戏等，从而减少或消除因更新而造成的服务中断时间。\n热更新有不同场景，常见的如：\n代码热替换\n动态替换或更新应用程序中的一部分代码。这通常需要特定的编程语言支持或运行时支持，如 Java 的类加载机制或 Go 的插件系统（其实无法实现）。\n资源热更新\n在不更改任何执行代码的情况下，更新应用程序使用的资源文件，如配置文件、图像或其他媒体资源。\n状态热迁移\n在更新过程中，将应用程序的状态从旧版本迁移到新版本，确保数据的连续性和一致性，如要考虑登录态、连接状态、执行中的事务等等。\n简单归纳，这三种场景分别主要作用于代码层、资源层和逻辑层。而不同的场景有不同的方案，而后两者具有语言无关性。\n实现方案 # 本文将主要关心的是第一种场景，即与编程语言相关的方案。具体描述为，如何在 Go 中动态替换或者说更新应用中的一部分代码。\nGo 语言（通常被称为 Golang）在设计上是一种静态、编译型的语言。这意味着 Go 程序在运行前要被编译成机器代码。相比动态语言，静态编译型语言在实现热更新方面面临更多挑战。不过还是想尝试下 Go 能否可以实现热更新。\n我们上面提到 Go 中实现这个代码层面的热更新能力，要借助于一个叫 plugin 系统的技术，我在网上搜索了半天，也是这个方案。不过我提前打个预防针，我的测试告诉我，Go 的插件机制其实不支持这个能力。\ngo 的 plugin 机制是从 go1.8 引入，是一个实验特性。 支持的是系统是类 Unix 系统（Linux 和 MacOS），不支持 win。 只能加载不能卸载，且加载内容无法修改。 主要是最后一点，不支持 plugin 库的重载和卸载，我们就无法用它实现热更新了。Go 本身是基于静态库编译，这是它的优势，易于分享部署和发布。而这个 plugin 动态库机制，就只有动态库节省内存这个不是优势的优势。\n不仅感慨，怪不得看到不少评论说 Go 的插件机器很鸡肋。\n如果你关心验证过程，可继续源码实现部分。\n开始验证 # Go 1.8 引入的这个的插件系统（plugin 包），允许 Go 程序动态地加载其他编译好的 Go 代码作为插件。这个机制可以用来实现某种形式的热更新：\n如何实现呢？\n假设，我们要实现一个名为 greetings.so 的插件，源码文件是 greetings.go，部分源码如下所示：\n//export Greet func Greet(name string) { fmt.Println(\u0026#34;Hello,\u0026#34;, name, \u0026#34;from the plugin!\u0026#34;) } 为了将其编译为一个插件，我们要使用 -buildmode=plugin 选项编译。\n$ go build -o greetings.so -buildmode=plugin greetings.go 在程序中加载这个插件，核心代码如下所示：\nfunc main() { // 加载插件 plug, err := plugin.Open(\u0026#34;greetings.so\u0026#34;) if err != nil { log.Fatal(err) } // 查找插件中的Greet符号 symGreet, err := plug.Lookup(\u0026#34;Greet\u0026#34;) if err != nil { log.Fatal(err) } // 断言Greet的类型 var greetFunc func(string) greetFunc, ok := symGreet.(func(string)) if !ok { log.Fatal(\u0026#34;Plugin has no \u0026#39;Greet(string)\u0026#39; function\u0026#34;) } // 使用字符串参数调用Greet函数 greetFunc(\u0026#34;World\u0026#34;) } 运行程序，输出如下：\n$ go run main.go Hello, World from the plugin! 是我们预期的结果。\n尝试热更新 # 既然，我们能在主程序动态加载 .so 文件，那是不是就能通过检查 .so 文件的状态，确定是否要重新加载这个代码片段呢？\n基本思路：加载 .so 文件时，记录其更新时间，在每次调用它实现的函数时，检查当前 .so 文件的更新时间，如果大于最新加载时间，重新加载执行即可。\n我们可以定义个结构体，管理在 greetings.so 中的所有函数。\n// Greetings 管理greetings插件的加载和调用 type Greetings struct { Path string // 插件文件路径 lastModTime time.Time // 插件最后更新时间 greetFunc func(string) // Greet 函数引用 } // NewGreetings 创建并返回一个新的 Greetings 实例 func NewGreetings(pluginPath string) *Greetings { return \u0026amp;Greetings{Path: pluginPath} } 实现一个内部方法，在调用 .so 文件中的函数时，检查插件库的更新状态，如果发现当前的库更新时间大于之前加载时的更新时间，重新加载。\n// tryLoadPlugin 尝试加载或重新加载插件 func (g *Greetings) tryLoadPlugin() { info, err := os.Stat(g.Path) if err != nil { log.Fatal(\u0026#34;Failed to stat plugin file:\u0026#34;, err) } modTime := info.ModTime() // 如果插件文件有更新，则重新加载插件 if modTime.After(g.lastModTime) { log.Println(\u0026#34;Detected plugin update, reloading...\u0026#34;) g.lastModTime = modTime plug, err := plugin.Open(g.Path) if err != nil { log.Fatal(\u0026#34;Failed to open plugin:\u0026#34;, err) } symGreet, err := plug.Lookup(\u0026#34;Greet\u0026#34;) if err != nil { log.Fatal(\u0026#34;Failed to find Greet symbol:\u0026#34;, err) } var ok bool g.greetFunc, ok = symGreet.(func(string)) if !ok { log.Fatal(\u0026#34;Plugin has no \u0026#39;Greet(string)\u0026#39; function\u0026#34;) } } } 现在，将 Greet 添加为 Greetings 结构体的方法即可，实现起来非常简单，如下所示：\n// Greet 调用插件中的 Greet 函数 func (g *Greetings) Greet(name string) { g.tryLoadPlugin() // 首次运行或插件更新后，尝试加载插件 if g.greetFunc != nil { g.greetFunc(name) // 调用插件中的 Greet 函数 } else { log.Println(\u0026#34;Greet function not available.\u0026#34;) } } 我尝试修改了函数中的打印内容：\n//export Greet func Greet(name string) { fmt.Println(\u0026#34;Hello,\u0026#34;, name, \u0026#34;from the plugin v1!\u0026#34;) } 我测试后发现，输出显示的确监听到了 .so 的更新，但在重新载入后，打印的依旧是之前版本的信息。\n如果你执着于 plugin 实现热更新，或许还有一个方法可尝试。既然不能卸载，那可以直接加载不同名的 .so 库，替换掉原来的插件。考虑它只能存在于实验中，我就不继续尝试了。\n其他策略 # 不能通过 plugin 实现热更新的话，我们也有其他方式可用的，如采用服务重启或者利用微服务架构来减少更新对用户的影响。\n快速重启\n通过优化应用的启动时间和状态恢复逻辑，实现快速重启，从而减少服务不可用的时间。\n微服务架构\n将应用分解为多个小型服务，每个服务独立部署和更新。这样，更新某一部分的服务时，只会影响到该服务，而不会影响到整个应用。这也算是另一种程序上代码热更新了。\n还可以与其他策略配合，如下是一些主流的思路。\n代理和版本控制\n使用代理服务器来控制流量，根据请求的版本号动态地路由到不同版本的服务实例。这样可以同时运行多个版本的服务，并逐渐将用户流量迁移到新版本，实现无缝更新。\n容器编排\n利用 Docker、Kubernetes 等容器和编排工具可以更容易地实现服务的滚动更新，尽管这不是热更新的传统意义，但它提供了类似的用户体验，减少了更新过程中的停机时间。\n总结 # 综上所述，Go 在设计上不是为热更新而设计的，它的 plugin 系统确实很鸡肋。\n如果要实现热更新，通过一些通用策略和工具，还是可以实现类似热更新的效果，尤其是在微服务架构中。可根据具体的应用场景和需求，选择最合适的更新策略。\n","date":"2024-04-09","externalUrl":null,"permalink":"/posts/2024-04-09-try-hot-update-using-plugin-package-in-golang/","section":"文章","summary":"昨天发了一篇名为 “entr 一个通用的热重启方案” 的文章，写完这个命令的简单使用后，我开始思考一个问题：Go 这样的静态编译型语言是否能实现热更新？如果能，该如何实现呢？\n","title":"我想用 Go 的 plugin 机制实现热更新，我失败了","type":"posts"},{"content":"在开发类似于 web 或其他常驻服务时，我们在修改代码后，要手动重启才能更新服务。如果你不是这种情况，或许框架默认支持热重启或是你集成了其他工具。\n本文将介绍一款工具，它能轻松实现简单的热重启，它具有语言和框架无关性，是一个通用小工具，它就是 entry。\n特别说明，这个工具主要是用在开发调试阶段，不支持复杂的热重启能力。\n什么 entry # 简单来说，它是一个可监听文件状态变化并执行特定动作的命令。让我们直接通过演示观察它的行为。\n$ ls text.txt | entr echo \u0026#34;file changed\u0026#34; 我们通过 ls text.txt 告诉 entry 监听的文件。当编辑并保存文件后，它通过指定命令 echo 打印提示 file changed。\n我们只要对它稍做修改，就可以实现在监听到文件变化后，自动执行 停止服务 -\u0026gt; 重新编译 -\u0026gt; 启动服务 等一系列动作。\n安装 # # mac 安装命令 brew install entr 实现热重启 # 首先，开发一个简单 Go server 服务，文件是 main.go，代码如下：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte(\u0026#34;Hello World!\u0026#34;)) }) fmt.Println(\u0026#34;Server is listening on :3000\u0026#34;) http.ListenAndServe(\u0026#34;:3000\u0026#34;, nil) } 为这个服务加上热重启能力，命令如下：\nfd -e go | entr -r go run *.go 重启服务 我们通过 fd 遍历所有 go 源码文件，当发现文件更新后会重新执行 go run *.go。注意，上述命令中，我们使用了 entr 的 -r 实现 reload，它会发送停止信号给常驻服务，让其重新运行。\n如果你希望每次重新启动后，执行清屏日志，也可使用 -c 选项。\n创建新文件 # 但这还有一个缺点，当创建新文件，entry 默认无法检测。针对这个场景，entry 提供了一个选项 -d，它在检测新文件后，停止 entr 命令。由于是停止重新执行，我们要通过 while 循环才能重启服务。\n命令如下：\nwhile true do fd *.go | entr -d -r go run *.go; done 看起来已经足够使用，但它有个问题，无法退出服务。因为即使是强制 kill 掉 go run 进程，依然会循环重启。我们要再引入一个命令 - trap，捕捉到退出信号停止循环，退出程序。\ntrap \u0026#34;echo \u0026#39;command you want to execute\u0026#39;\u0026#34; SIGINT; while true do sleep 10; done 这里的效果是捕捉 SIGINT 信号，并打印 echo \u0026quot;command you want to execute\u0026quot;。\n效果如下所示：\n如此的话，简单改造下前面的命令。\n现在，在源码根目录下创建一个名为 run.sh 的文件，源码如下所示：\n#!/bin/bash trap \u0026#34;exit;\u0026#34; SIGINT; while true; do fd -e go | entr -rcd go run *.go; done 现在，我们通过 run.sh 启动服务即可，我们为服务添加了一个实时构建编译和简单的热重启能力。\n限制和考虑 # 虽然 entr 能够实现一种简单的热重启机制，但它并不具备复杂的状态管理或零停机更新的能力。\n对于需要更高级热更新或热重载功能的应用，可能需要使用更专门化的工具或框架来实现，这些工具或框架能够处理如状态迁移、版本兼容性等更复杂的场景。\n最后 # 总体而言，不同语言或框架可能都有自己的管理工具，但通过 entr 命令，我们能以最简单的方式实现热重启这个需求。\n希望本文对你有所帮助，感谢阅读。\n","date":"2024-04-08","externalUrl":null,"permalink":"/posts/2024-04-08-hot-restart-service/","section":"文章","summary":"在开发类似于 web 或其他常驻服务时，我们在修改代码后，要手动重启才能更新服务。如果你不是这种情况，或许框架默认支持热重启或是你集成了其他工具。\n本文将介绍一款工具，它能轻松实现简单的热重启，它具有语言和框架无关性，是一个通用小工具，它就是 entry。\n","title":"entry，一个语言无关的热重启方案","type":"posts"},{"content":"最近尝试了一款内置 AI 能力的终端软件，名为 Warp，它的交互设计非常不错，很值得上手。但它的主要问题是中文支持不够友好，且我也不希望 AI 的能力被限制在某款终端上。\n在这个背景下，我开始尝试从网上搜索一些将 AI 引入终端的命令，发现了两款不错的软件，一个就是今天要介绍的这款名为 tgpt 的命令，无 API Key 要求，可以免费用。\n本文带大家快速体验下这个命令。另特别提示，如遇网络问题，自行处理，\n介绍 # tgpt 是一个可以在终端上使用 ChatGPT，我觉得，它的最大卖点是无需 API Key 即可使用，毕竟免费才是王道。\n提到免费，ChatGPT 最近开放了无需注册即可使用，有条件的可以去体验下。\ntgpt 的默认模型是 phind，是一款对开发者友好的大模型。tgpt 支持多种模型，包括但不限于 KoboldAI、Phind、Llama2、Blackbox AI，以及需要 API 密钥的 OpenAI 所有模型和 Craiyon V3 图像生成模型。\n接下来，开始快速安装体验下吧。\n安装 # curl -sSL https://raw.githubusercontent.com/aandrew-me/tgpt/main/install | bash -s /usr/local/bin 使用 # 它的使用方法也非常简单，只要在终端中输入如下命令：\ntgpt \u0026#34;What is internet?\u0026#34; 或者，你也可以使用指定的模型：\ntgpt --provider opengpts \u0026#34;What is 1+1\u0026#34; 场景 # tgpt 另一个不错的点是在于它默认提供了三个场景的支持，分别是生成 shell 命令、代码和图片。\n我们来具体测试下。\n生成 shell 命令\nWarp AI 的一大亮点就是自动生成命令的能力\n生成命令后，提示我们是否直接执行。这肯定是终端场景下的必备能力。\n生成 Code\n最后，生成图片的效果就不展示了，效果上不错好，估计是提示词的要求比较高，要看下如何使用它的图片生成模型 Craiyon V3。\n对话 # tgpt 还支持对话模式，可以使用 tgpt -i 进入对话模式。\n这样一来，对于大部分场景下，我们都不用开启浏览器了，省心啊。\n最后 # 我觉得 tgpt 这个命令的最大优势在于它可以免费使用，而且提供了对程序员友好的一些模型和使用场景。初步看了下，它好像是没有提供自定义 role 的能力，这算是它的一个缺点吧。\n","date":"2024-04-03","externalUrl":null,"permalink":"/posts/2024-04-03-aichat-in-terminal/","section":"文章","summary":"最近尝试了一款内置 AI 能力的终端软件，名为 Warp，它的交互设计非常不错，很值得上手。但它的主要问题是中文支持不够友好，且我也不希望 AI 的能力被限制在某款终端上。\n","title":"我在终端上免费使用 GPT","type":"posts"},{"content":"今天写一篇小短文，推荐两个 Chrome 插件，用于程序员们提高浏览器的操作效率，让我能像使用终端一样使用浏览器。\nCTRL+N/P # 一个是 CTRL+N/P，如果你习惯于终端上的上下翻页历史的记录的操作，这个工具挺适合你的。它的作用也非常简单，但你在搜索引擎上搜索记录时，如果遇到刻在骨子里的情不自禁使用 CTRL+P/N 上下翻页搜索相关条目是，它就是你需要的。\n默认情况下，Chrome 浏览器顶部搜索框是支持这个操作的。\n但如果进入搜索页面，这个搜索框没有这个能力。\n于是我就尝试安装了这个 Ctrl+N/P 插件。\n我在 Google、baidu、bing 和 sogou 进行了测试，结果除了 baidu 其他的都可以正常使用。这对我没有影响，毕竟我基本不用 baidu。\nVimium # 另一个插件是 Vimium，适合于 Vim 用户的 Chrome 插件，浏览器上的 vim 模拟器。如果你习惯于日常使用 Vim, 然而发现和浏览器交互时, 就必须要使用鼠标或者触控板，可以考虑使用它。\n它的使用不复杂。默认情况下，它是在 Vim 的 Normal 模式，如果要想把浏览器恢复到和没有安装 Vimium 一样的效果，只要输入 i 到插入模式即可。在\nNormal 模式下的大部分操作都是 Vim 风格的。输入 ? 就可以打开它的帮助文档。\n我最喜欢的还是 Vim 的导航能力，这也是平时用的最多的。阅读文档或者阅读测试代码时，直接 jk 实现上下滚动，与双屏配合，大部分情况下无需用到鼠标。\n如果想快速点击或访问链接快速，输入 f，它会把所有可点击标识出来。我测试过，它也不能把所有的点都能标识出来，有它的局限性。\n具体效果如下：\nVimium 已经很大程度上提高了键盘操作浏览器的效率，它还有更多能力，在它的帮助中都能找到，输入 ? 可自行探索吧。\n最后 # 我觉得也不要过分迷恋这种操作，毕竟它无法实现完全的全键盘操作，GUI 毕竟和字符界面是不一样的，找到适合自己操作方式。\n但其实我挺迷恋的，毕竟不迷恋就不会研究这操作。\n附上帮助翻译 # 快捷键 功能 j, \u0026lt;c-e\u0026gt; 向下滚动 k, \u0026lt;c-y\u0026gt; 向上滚动 gg 滚动到页面顶部 G 滚动到页面底部 d 向下滚动半页 u 向上滚动半页 h 向左滚动 l 向右滚动 zH 全部向左滚动 zL 全部向右滚动 r 重新加载页面 yy 复制当前URL到剪贴板 p 在当前标签页中打开剪贴板的URL P 在新标签页中打开剪贴板的URL gu 上升到URL层级结构 gU 转到当前URL层级结构的根 i 进入插入模式 v 进入视觉模式 V 进入视觉行模式 gi 聚焦页面上的第一个文本输入框 f 在当前标签页中打开一个链接 F 在新标签页中打开一个链接 \u0026lt;a-f\u0026gt; 在新标签页中打开多个链接 yf 复制链接URL到剪贴板 [[ 跟随标记为previous或\u0026lt;的链接 ]] 跟随标记为next或\u0026gt;的链接 gf 选择页面上的下一个框架 gF 选择页面的主要/顶部框架 m 创建一个新的标记 ` 跳转到一个标记 o 打开URL、书签或历史条目 O 在新标签页中打开URL、书签或历史条目 b 打开一个书签 B 在新标签页中打开一个书签 T 搜索你打开的标签页 ge 编辑当前的URL gE 编辑当前的URL并在新标签页中打开 / 进入查找模式 n 向前循环到下一个查找匹配 N 向后循环到上一个查找匹配 H 后退历史 L 前进历史 t 创建新标签页 J, gT 向左移动一个标签页 K, gt 向右移动一个标签页 ^ 跳转到之前访问的标签页 g0 跳转到第一个标签页 g$ 跳转到最后一个标签页 yt 复制当前标签页 \u0026lt;a-p\u0026gt; 固定或取消固定当前标签页 \u0026lt;a-m\u0026gt; 静音或取消静音当前标签页 x 关闭当前标签页 X 恢复关闭的标签页 W 将标签页移动到新窗口 \u0026lt;\u0026lt; 将标签页向左移动 \u0026gt;\u0026gt; 将标签页向右移动 ? 显示帮助 gs 查看页面源代码 如有错误，请指正。感谢！\n","date":"2024-04-01","externalUrl":null,"permalink":"/posts/2024-04-01-powerful-plugins-in-chrome/","section":"文章","summary":"今天写一篇小短文，推荐两个 Chrome 插件，用于程序员们提高浏览器的操作效率，让我能像使用终端一样使用浏览器。\nCTRL+N/P # 一个是 CTRL+N/P，如果你习惯于终端上的上下翻页历史的记录的操作，这个工具挺适合你的。它的作用也非常简单，但你在搜索引擎上搜索记录时，如果遇到刻在骨子里的情不自禁使用 CTRL+P/N 上下翻页搜索相关条目是，它就是你需要的。\n","title":"如何像使用终端一样使用浏览器？","type":"posts"},{"content":"我对 TUI 的兴趣源于一个名为 LazyGit 的终端命令，它让我只需几次按键即可完成一次 Git 提交，而不是 zsh 的 git 插件那种通过 alias 的方式实现。\n不同于 GUI，TUI 低系统资源消耗和高效的全键盘操作，这让我对如何开发这类应用产生了兴趣。\n最终，我找到了 Bubbletea 这个框架。一个月前，还写了一篇基于 Bubbletea 开发 TUI 命令的文章。当时的我对这个框架的认识并不深入，仅限于开发一些简单小程序，如果想开发一个如 LazyGit 般复杂的程序时，就没有那么容易了。\n今天的文章重点介绍 bubbletea 的一系列扩展库，和 bubbletea 一样是位于 github.com/charmbracelet 下，由 charmbracelet 团队开发的其他开源库。\n这些扩展库 star 数基本都在千以上。说实在的，很少遇到核心框架之外，扩展库的 star 也基本在千以上的。我觉得主要因为这些库的普适性，基本与 bubbletea 的关系都只是互补关系，而不是强依赖，任意一个库都能拿出来单独介绍。\nName description BubbleTea Go 编写的 TUI 框架，灵感源于 Elm 架构，是构建复杂 TUI 应用的基础。 Bubbles 一套标准的TUI组件，如进度条、文本输入等，与 BubbleTea 无缝集成。 Lipgloss 用于终端中创建富文本布局，支持边框、间距、对齐、样式和颜色等。 Glamour 渲染 Markdown 文本的库，使得在 TUI 应用显示格式化的文档变得简单。 Glow 渲染 Markdown 文件的命令工具，支持多种主题，适合用于终端阅读。 Gum 提供了一系列快速创建交互式组件的命令，Shell 脚本即可开发 TUI 应用。 Soft Serve 自托管 Git 服务器，TUI 界面，命令行管理和分享 Git 仓库变得更简洁。 Wish 一个构建 SSH 应用的库，可创建 TUI 版远程应用，通过 SSH 暴露服务。 Harmonica 一个简单高效的弹簧动画库，用于创建平滑和自然的动画效果的所需参数。 Mods 将大型语言模型（LLM）的能力引入到命令行和管道工作流中的命令工具。 VHS 用于录制终端会话的工具，允许捕获命令行操作，便于分享、教学或演示。 Pop 终端邮件发送工具，可通过标准输入传递邮件内容，更易实现自动化邮件。 Log 一个彩色日志库，支持多种日志级别和格式，方便终端的日志展示和追踪。 Bubble Tea 核心框架 # Bubbletea 是这个生态的核心框架，它是我们开发 TUI 应用的起点。\nBubbletea 基于模型-视图-更新（MVU）架构（The Elm Architecture）。具体细节可查看前文介绍，其中对 bubbletea 的基本架构做了相对详细的说明。\n如下是源于官方指南中的 HelloWorld 案例，基于 bubbletea 创建一个简单的计数器。\ntype model int // 初始化模型，设置计数器的初始值 func initialModel() model { return 0 } func (m model) Init() tea.Cmd { return nil } // 更新函数，根据消息更新模型 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case \u0026#34;q\u0026#34;: return m, tea.Quit case \u0026#34;+\u0026#34;: return m + 1, nil case \u0026#34;-\u0026#34;: return m - 1, nil } } return m, nil } func (m model) View() string { return fmt.Sprintf(\u0026#34;Count: %d\\nPress + to increment, - to decrement, q to quit.\\n\u0026#34;, m) } func main() { p := tea.NewProgram(initialModel()) if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, \u0026#34;Error running program: %v\u0026#34;, err) os.Exit(1) } } 效果如下：\n一些基础扩展 # 随着我对 Bubble Tea 框架的深入，需求也在变多，我开始了解其它 bubbletea 衍生而来的扩展库。我觉得首先是 3 个极可能受到大家关心的库，分别是布局与样式 - Lipgloss，组件库 - bubbles，交互式 form 表单 - huh\nLipgloss，它提供了定义样式和布局的能力。如通过它调整颜色、边框和布局等，轻松实现美观实用的界面。Lipgloss 能极大地提升 TUI 应用的外观和用户体验。\nstyle := lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). // 圆角边框 Bold(true). // 加粗 Foreground(lipgloss.Color(\u0026#34;#FAFAFA\u0026#34;)). // 前景色 Background(lipgloss.Color(\u0026#34;#7D56F4\u0026#34;)). // 背景色 PaddingTop(2). // 顶部 padding PaddingLeft(4). // 左侧 padding AlignVertical(lipgloss.Center). // 垂直居中 Height(5). // 高度 Width(40) // 宽度 // 水平布局 left := style.Render(\u0026#34;Hello Left!\u0026#34;) right := style.Render(\u0026#34;Hello Right\u0026#34;) fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, left, right)) // 垂直布局 top := style.Render(\u0026#34;Hello Top!\u0026#34;) bottom := style.Render(\u0026#34;Hello Bottom!\u0026#34;) fmt.Println(lipgloss.JoinVertical(lipgloss.Top, top, bottom)) Bubbles，它为 Bubbleta 提供了基础组件。让我能轻松添加各种 UI 组件，如进度条和文本输入框，这些都是创建交互式命令行应用不可或缺的元素。\n组件的示例代码要结合 bubbletea 框架，我就不粘贴大段的代码了。\n文本输入框： 表格： 进度条： 更多就不一一展示了，查看 bubbles 仓库地址即可。\nHuh，为 Bubble Tea 提供了构建交互式表单和提示的能力。相对于 bubbles 中的基础组件，它使我们能轻松实现各种用户输入界面，从选择列表到文本输入框，再到确认对话框。\n和 Lipgloss 一样，Huh 也是可脱离 bubbletea 使用的。\n一个简单案例：\nvar name string _ = huh.NewInput(). Title(\u0026#34;What\u0026#39;s your name?\u0026#34;). Value(\u0026amp;name). Run() // this is blocking... fmt.Printf(\u0026#34;Hey, %s!\\n\u0026#34;, name) Huh 官方 README 文档中提供了一个完整的购买汉堡的订单流程。\n高级工具库 # 除了上述的基础库，charmbracelet 下还有一些高级的工具库，如 markdown 渲染器 - glamour 和动画制作 - Harmonica。唯一可惜的是，没找到图表库。\nGlamour，它为了终端显示 markdown 文档提供了支持，它支持样式自定义，提供了几种预定义主题风格。\nin := `# Hello World This is a simple example of Markdown rendering with Glamour! Checkout the [other examples](https://github.com/charmbracelet/glamour/tree/master/examples) Bye! ` r, _ := glamour.NewTermRenderer(glamour.WithAutoStyle(), glamour.WithWordWrap(40)) out, err := r.Render(in) if err != nil { log.Fatal(err) } fmt.Print(out) 诸如 github、gitlab 和 gitee 的官方 CLI 工具，用的都是这个库实现的 markdown 渲染。\n他们还提供了一个基于 Glamour 实现的 glow 命令行工具，支持查看本地或远程的 Markdown 文档。\n我原本想找一个能和 bubbletea 集成的图表组件库，但只发现了一个名为 Harmonica 的库。\nHarmonica 主要是用于模拟弹簧运动，它本身并不提供绘制逻辑，只会生成物理的运动参数（如位置和速度），以实现平滑和自然的动画效果。但它最近已经没更新了。\n官方提供的一个案例效果：\nSHELL 脚本编写 TUI # 我要感慨下，之前看到很多用 rust 写的 CLI 工具，羡慕不已，Go 原来也有这么好用或者说是比之更佳的库。\n如果你对 rust 和 Go 都不了解，但想实现一个 TUI 应用，我要推荐这个命令- Gum，它让我们通过 SHELL 脚本即可实现 TUI 应用。\nGum 提供了一系列工具，实现基于 Shell 脚本即可编写一些小巧的 TUI 应用，有助于优化我们的日常工作流。\n我们来看看如下是一些简单用法说明：\n输入（input）: 提示用户输入文本。 gum input --placeholder \u0026#34;Enter your name\u0026#34; 选择（choose）: 创建一个交互式的列表让我们从中选择一个选项。 gum choose \u0026#34;Option 1\u0026#34; \u0026#34;Option 2\u0026#34; \u0026#34;Option 3\u0026#34; 过滤（filter）: 从输入的列表中过滤出符合条件的项。\necho -e \u0026#34;one\\ntwo\\nthree\u0026#34; | gum filter 确认（confirm）: 弹出一个确认对话框让我们选择确认。\ngum confirm \u0026#34;Are you sure?\u0026#34; \u0026amp;\u0026amp; echo \u0026#34;I\u0026#39;m sure\u0026#34; || echo \u0026#34;No\u0026#34; 旋转指示器（spin）: 在执行耗时长的命令时显示旋转指示器。\ngum spin -- sleep 5 表格（table）: 渲染一个数据表格。\ngum table --header \u0026#34;Name,Email\u0026#34; --rows \u0026#34;Alice,alice@example.com\\nBob,bob@example.com\u0026#34; 文件选择（file）: 从文件系统中选择一个文件。\ngum file | xargs glow -p 其他如样式定制，布局等能力都是支持的，就不一一展示了，有兴趣可以看它的 README 文档。\n毫无疑问，Gum 增强了 Shell 脚本。通过 Gum 这些命令，可以编写出体验更佳的交互式命令。Gum 的每个命令的帮助信息都可通过 gum \u0026lt;command\u0026gt; --help 查看。\n官方案例中有一个交互式的 Git Commit 工作流。\n完整脚本实现也很简单。\n#!/bin/sh TYPE=$(gum choose \u0026#34;fix\u0026#34; \u0026#34;feat\u0026#34; \u0026#34;docs\u0026#34; \u0026#34;style\u0026#34; \u0026#34;refactor\u0026#34; \u0026#34;test\u0026#34; \u0026#34;chore\u0026#34; \u0026#34;revert\u0026#34;) SCOPE=$(gum input --placeholder \u0026#34;scope\u0026#34;) # Since the scope is optional, wrap it in parentheses if it has a value. test -n \u0026#34;$SCOPE\u0026#34; \u0026amp;\u0026amp; SCOPE=\u0026#34;($SCOPE)\u0026#34; # Pre-populate the input with the type(scope): so that the user may change it SUMMARY=$(gum input --value \u0026#34;$TYPE$SCOPE: \u0026#34; --placeholder \u0026#34;Summary of this change\u0026#34;) DESCRIPTION=$(gum write --placeholder \u0026#34;Details of this change (CTRL+D to finish)\u0026#34;) # Commit these changes gum confirm \u0026#34;Commit changes?\u0026#34; \u0026amp;\u0026amp; git commit -m \u0026#34;$SUMMARY\u0026#34; -m \u0026#34;$DESCRIPTION\u0026#34; SSH 的远程 TUI 应用 # 你是否想过 TUI 应用也能像 Web 服务那样通过远程访问呢？\n通过一款名为 wish 的库，可轻松开发可远程访问的 TUI 应用。wish 的传输协议是 SSH，于用户而言，只要在终端执行一个 SSH 命令，即可访问部署在远程的应用。\n如下是远程访问 charmbracelet 团队的 tui 版 Git Server。\nssh git.charm.sh 如果你也想拥有这样的一个 git server，可直接部署 charmbracelet/Soft-Serve 这个项目到你的机器上。或者如果你对开发这样的应用有兴趣，官方提供了一些案例，查看地址 examples，其中就有一个 gitserver 的案例。\n还有，charmbracelet 团队还开发一个名为 wishlist 的应用，可作为 ssh 应用的展示目录。\n它不限制是否是 wish 开发的应用。如上所示，我可以将我的 SSH 机器的绑定其中。\n其他 # 除了上述介绍的这些，charmbracelet 下还有其他的一些值得推荐的库。如：\nvhs，一个终端操作录制命令，提供了一种简单高效的方式捕捉和分享终端操作。我这篇文章的 GIF 基本都是通过它录制的。\n它的使用很简单，创建一个 record.tape 文件，内容如下：\nSet TypingSpeed 500ms Type \u0026#39;echo \u0026#34;Hello VHS\u0026#34;\u0026#39; Enter Sleep 2s 执行如下命令即可生成一个 gif 文件。\n$ vhs record.tape 生成效果：\nmods，一个 LLM GPT 的终端命令，可对接市面主流的大模型 AI，如 openai、grok、localai 等。我用了下体验还不错。\npop，一个支持在终端接发邮件的命令，操作起来高效便捷。\nLog，一个支持彩色输出的 Go 日志库，可提高我们日常的调试体验和阅读的舒适度。\n结语 # 这次深入 BubbleTea 及其生态中的其他组件，不仅学习了如何开发功能丰富的TUI应用，还知道如何使这些应用更加美观与实用。Bubble Tea 生态不仅提供了强大的工具和库，还激发了我对命令行应用新可能性的想象，有趣的经历。\n","date":"2024-03-15","externalUrl":null,"permalink":"/posts/2024-03-15-tui-app-using-bubbletea/","section":"文章","summary":"我对 TUI 的兴趣源于一个名为 LazyGit 的终端命令，它让我只需几次按键即可完成一次 Git 提交，而不是 zsh 的 git 插件那种通过 alias 的方式实现。\n不同于 GUI，TUI 低系统资源消耗和高效的全键盘操作，这让我对如何开发这类应用产生了兴趣。\n","title":"推荐 bubbletea 扩展库开发 TUI 应用","type":"posts"},{"content":"上篇介绍 GO 的 GUI 库 Fyne 时，提到 Fyne 的数据绑定用到了监听器模式。本文就展开说下我对 Go 中监听器模式的理解和应用吧。\n监听器模式简介 # 监听器模式，或称观察者模式，它主要涉及两个组件：主题（Subject）和监听器（Listener）。\nSubject 负责维护一系列的监听器，在所观测主题状态变化，将这个事件通知给所有注册的监听器。我统一将其定义为注册中心 Registry。而监听器 Listener 则是实现了特定接口的对象，用于响应事件消息，执行处理逻辑。\n对具体应用而言，通常还会分出一个 Watcher 或者 Monitor 用于检测变化并推送给 Registry。从而实现将检测目标从系统解耦，无视监控组件类别。\n这个模式在组件之间建立一种松散耦合的关系。将特定事件通知到关心它的其他组件，无需它们直接相互引用。看起来这个不也是发布-订阅模式吗？差不多一个意思。\n之前工作中，用它最多的是配置的热更新场景，这篇文章也会简单介绍基于它的 ETCD 配置热更新。\nGo 实现监听器模式 # 如何用 Go 实现监听模式？我将定义两个新类型分别是注册中心（Registry）和监听器接口（Listener）。\ntype Listener interface { OnTrigger() } 首先是 Listener，它是一个接口，用于实现事件的响应逻辑。它的实现类型要求支持 OnTrigger 方法，会在事件发生时被执行。\ntype Registry struct { listeners []Listener } func (r *Registry) Register(l Listener) { r.listeners = append(r.listeners, l) } func (r *Registry) NotifyAll() { for _, listener := range r.listeners { listener.OnTrigger(key, value) } } Registry 是所有监听器的注册地，当特定事件发生，我们通过 Registry.NotifyAll 将事件传递给所有 Listener。\n让我们实现一个简单的案例，当监听到某个事件发生，打印 \u0026ldquo;A specified event accured\u0026rdquo;。本案例的事件将先通过主函数调用 NotifyAll 模拟触发事件。\n为了打印事件发生消息。创建新类型实现 Listener：\ntype EventPrinter struct { } func (printer *EventPrinter) OnTrigger() { fmt.Println(\u0026#34;A specified event accured!\u0026#34;) } 写个主函数测试下是否符合预期，代码如下所示：\nfunc main() { r := \u0026amp;Registry{} r.Reigster(\u0026amp;EventPrinter{}) // 模拟接收到消息，触发事件通知 r.NotifyAll() } 让我们执行测试，内容如下所示：\n$ go run main.go A specified event occurred 如果希望自定义接受函数，只需在 Listener 支持自定义事件接收函数即可。\ntype EventHandler struct { callback func() } func NewEventHandler(callback func()) *EventHandler { return \u0026amp;EventHandler{callback: callback} } func (e *EventHandler) OnTrigger() { e.callback() } 注册一个 EventHandler 到 Registry，主函数代码：\nfunc main() { r := \u0026amp;Registry{} r.Reigster(\u0026amp;EventPrinter{}) r.Reigster(NewEventHandler(func() { fmt.Println(\u0026#34;Custom Print: a specified event occurred!\u0026#34;) })) r.NotifyAll() } 测试执行：\n$ go run main.go A specified event occurred! Custom Print: a specified event occurred! 基于 Go Channel 实现并发处理 # 前面的示例中 NotifyAll 是通过 for 循环依次调用 listener.OnTrigger 将消息发送给 Listener，处理效率低下。如何加速呢？最直接的方法是通过 goroutine 运行 listener.OnTrigger 方法。\nfunc (r *Registry) NotifyAll() { for _, listener := range r.listeners { go listener.OnTrigger() } } 还有一种方法，通过 Channel 传递事件消息，这样每个 Listener 有独立的 goroutine 监听和处理。\n如下是 Listener 的实现代码：\ntype Listener struct { EventChannel chan struct{} Callback func() } func NewListener(callback func()) *Listener { return \u0026amp;Listener{ EventChannel: make(chan struct{}, 1), // 带缓冲的 channel，防止阻塞 Callback: callback, } } func (l *Listener) Start() { go func() { for range l.EventChannel { l.Callback() } }() } 这里 Listener 的事件处理函数在单独的 goroutine 中运行。而相应的 Registry 实现也需要修改，代码变更如下所示：\ntype Registry struct { listeners []*Listener } func (r *Registry) Register(listener *Listener) { r.listeners = append(r.listeners, listener) listener.Start() // 启动监听器的 goroutine } func (r *Registry) NotifyAll(message string) { for _, listener := range r.listeners { listener.EventChannel \u0026lt;- struct{}{} // 发送事件到监听器 } } func (r *Registry) Close() { for _, listener := range r.listeners { close(listener.EventChannel) // 关闭 channel，停止监听器 goroutine } } 整体上的变化不大，在 listner.Register 方法中启动 Listener 事件处理 goroutine 等待事件消息。\n实际案例：ETCD 配置热更新 # 让我们实践一个具体的应用场景：实现配置的动态更新以及组件的自动重连机制。\n我们将针对包括 MySQL、Redis 在内的各种组件，实现它们在配置变更时能够自动重连。这些组件的配置信息将以 JSON 格式存储于 ETCD 的多个键（Key）中。\n假设，配置结构如下所示：\ntype MySQLConfig struct { Host string Port int User string Password string } type RedisConfig struct { Host string Port int } 这些配置被保存在 ETCD 中，我们要实时监控配置的变化并据此更新配置和执行重连操作。\n示例用法如下所示：\nregistry.Register(\u0026#34;/config/mysql\u0026#34;, func(data) { // unmarshal data // reconnect mysql }) 让我们基于监听器模式简单设计一个模块，实现 ETCD 热更新：\n每个监听器可以订阅特定的 key 或 key 前缀的更新事件。 使用 channel 通知配置变更，触发对应的监听回调。 这个示例，函数回调和轮询其实已经满足需求，此处只是为了演示，而是否使用 channel 要具体分析。\n我们这个设计要涉及到三个部分。分别是 Watcher、Listener 和 Registry。\nWatcher 责监听 ETCD 中的 key 变更事件。 Listener 定义了当特定 key 发生变化时需要执行的回调逻辑。 Registry 管理所有 Listener，将 ETCD 变更事件分发给对应 Listener。 先定义 Event 类型，一个简单的结构体，表示 ETCD 中 key 的变更事件：\ntype Event struct { Key string Value string } Listener # Listener 实现如下所示：\ntype Listener struct { EventChannel chan *Event Callback func(*Event) } func NewListner(callback func(*Event)) *Listener { l := \u0026amp;Listener{ EventChannel: make(chan *Event), Callback: callback, } return l } func (l *Listener) Start() { go func() { for event := range l.EventChannel { l.Callback(event) } }() } 基本之前的没太大差别，从 EventChannel 中拿到事件消息，调用回调函数。\n实现 Registry # Registry 负责维护 Listener 的注册，并在接收到 key 变更事件时通知相关的 Listener：\ntype Registry struct { listeners map[string][]*Listener } func NewRegistry() *Registry { return \u0026amp;Registry{ listeners: make(map[string][]*Listener), } } func (r *Registry) Register(key string, listener *Listener) { r.listeners[key] = append(r.listeners[key], listener) listener.Start() } func (r *Registry) Notify(event *Event) { if listeners, ok := r.listeners[event.Key]; ok { for _, listener := range listeners { listener.EventChannel \u0026lt;- event } } } 注册 Listener 到 Registry 中，通过 map 将 key 与 Listener 关联起来。\n实现 Watcher # Watcher 负责从 ETCD 订阅 key 的变更事件，并将这些事件发送到 Registry 的 eventChannel 上：\nfunc WatchEtcdKeys(client *clientv3.Client, registry *Registry, watchKeys ...string) { for _, key := range watchKeys { go func(key string) { watchChan := client.Watch(context.Background(), key, clientv3.WithPrefix()) for wresp := range watchChan { for _, ev := range wresp.Events { event := \u0026amp;Event{ Key: string(ev.Kv.Key), Value: string(ev.Kv.Value), } registry.Notify(event) } } }(key) } } 使用示例 # 让我们实际在 main 函数上使用一下，观察行为是否正常。\nfunc main() { client, err := clientv3.New(clientv3.Config{ Endpoints: []string{\u0026#34;localhost:2379\u0026#34;}, }) if err != nil { log.Fatal(err) } defer client.Close() registry := NewRegistry() // 注册监听器 registry.Register(\u0026#34;/config/mysql\u0026#34;, NewListener(func(event * Event) { fmt.Println(event) // 执行数据重连之类的操作 })) // 开始监听 ETCD key 变更 WatchEtcdKeys(client, registry, \u0026#34;/config/\u0026#34;) time.Sleep(10 * time.Minute) } 这个示例创建了一个 ETCD 客户端，初始化了一个 Registry，并为特定的 key 注册了一个 Listener。然后，通过 WatchEtcdKeys 函数开始监听 /config/ 前缀下的所有 key 的变更。\n这种设计支持对特定 key 或 key 前缀的监听。当相关 key 变更时，通过 channel 通知 Listener，而收到更新事件后的具体操作。视场景而定，这里是执行重连操作。\n特别说明，示例仅作为概念验证，实际应用中需要更多的错误处理和优化。\n","date":"2024-03-09","externalUrl":null,"permalink":"/posts/2024-03-07-listener-design-pattern-in-golang/","section":"文章","summary":"上篇介绍 GO 的 GUI 库 Fyne 时，提到 Fyne 的数据绑定用到了监听器模式。本文就展开说下我对 Go 中监听器模式的理解和应用吧。\n监听器模式简介 # 监听器模式，或称观察者模式，它主要涉及两个组件：主题（Subject）和监听器（Listener）。\n","title":"Go 中的监视器模式与配置热更新","type":"posts"},{"content":"今天，推荐一个 Go 实现的 GUI 库 - fyne。\nGo 官方也没有提供标准的 GUI 框架，在 Go 实现的几个 GUI 库中，Fyne 算是最出色的，它有着简洁的API、支持跨平台能力，且高度可扩展。这也就是说，Fyne 是可以开发 App。\n本文将尝试介绍下 Fyne，希望对大家快速上手这个 GUI 框架有所帮助。我最近产生了不少想法，其中有些是对 GUI 有要求的，就想着折腾用 Go 实现，而不是用那些已经很流行和成熟的 GUI 框架。\n在写这篇文章时，顺手搞了下它的中文版文档，文档查看 www.poloxue.com/gofyne，希望能帮助那些想继续深入这个框架的朋友。\n安装 fyne # 开始前，确保已成功安装 Go，如果是 MacOS X 系统，要确认安装了 xcode。\n如下使用 go get 命令安装 Fyne。\n$ mkdir hellofyne $ cd helloyfyne $ go mod init hellofyne $ go get fyne.io/fyne/v2@latest $ go install fyne.io/fyne/v2/cmd/fyne@latest 如果想立刻查看 fyne 提供的演示案例，通过命令检查：\n$ go run fyne.io/fyne/v2/cmd/fyne_demo@latest 看起来，这里面的案例还是不够丰富的。\n安装工作到此就完成了。Fyne 对不同系统有不同依赖，如果安装过程中遇到问题，细节可查看官方提供的安装文档。\n创建第一个应用 # 由于 Go 简洁的语法和 fyne 的设计，使用 fyne 创建一个 GUI 应用异常简单。\n以下是一个创建基础窗口的例子：\npackage main import ( \u0026#34;fyne.io/fyne/v2/app\u0026#34; \u0026#34;fyne.io/fyne/v2/container\u0026#34; \u0026#34;fyne.io/fyne/v2/widget\u0026#34; ) func main() { a := app.New() w := a.NewWindow(\u0026#34;Hello Fyne\u0026#34;) w.SetContent(widget.NewLabel(\u0026#34;Welcome to Fyne!\u0026#34;)) w.ShowAndRun() } 这段代码创建了一个包含标签的窗口。\n通过 app.New() 初始化一个 Fyne 应用实例。然后，创建一个标题为 \u0026ldquo;Hello Fyne\u0026rdquo; 的窗口，并设置内容为包含 \u0026ldquo;Welcome to Fyne!\u0026rdquo; 文本标签。最后，通过w.ShowAndRun()显示窗口并启动应用的事件循环。\nfyne 中窗口的默认大小是其包含的内容决定，也可预置大小。\n如在创建窗口后，提供 Resize 重新调整大小。\nw.Resize(fyne.NewSize(100, 100)) 还有，一个 app 下可以有多个窗口的，示例代码：\nbtn := widget.NewButton(\u0026#34;Create a widnow\u0026#34;, func() { w2 := a.NewWindow(\u0026#34;Window 02\u0026#34;) w2.Resize(fyne.NewSize(200, 200)) w2.Show() }) w.SetContent(btn) 我们创建一个按钮，它的点击事件是创建一个新的窗口并显示。\n布局和控件 # 布局和控件是 GUI 应用程序设计中必不可少的两类组件。Fyne 提供了多种布局管理器和标准的 UI 控件，支持创建更复杂的界面。\n布局管理 # Fyne 中的布局的实现位于 container 包中。它提供了多种不同布局方式安排窗口中的元素。最基本的布局方式有 HBox 水平布局和 VBox 垂直布局。\n通过 HBox 创建水平布局的代码如下：\nw.SetContent(container.NewHBox(widget.NewButton(\u0026#34;Left\u0026#34;, func() { fmt.Println(\u0026#34;Left button clicked\u0026#34;) }), widget.NewButton(\u0026#34;Right\u0026#34;, func() { fmt.Println(\u0026#34;Right button clicked\u0026#34;) }))) 显示效果：\n通过 VBox 创建垂直布局的例子。\nw.SetContent(container.NewVBox(widget.NewButton(\u0026#34;Top\u0026#34;, func() { fmt.Println(\u0026#34;Top button clicked\u0026#34;) }), widget.NewButton(\u0026#34;Bottom\u0026#34;, func() { fmt.Println(\u0026#34;Bottom button clicked\u0026#34;) }))) 显示效果：\nFyne 除了基础的水平（HBoxLayout）和垂直（VBoxLayout）布局，还提供了GridLayout、FormLayout 甚至是混合布局 CombinedLayout 等高级布局方式。\n如 GridLayout可以将元素均匀地分布在网格中，而FormLayout适用于创建表单，自动排列标签和字段。这些灵活的布局选项支持创建更复杂和功能丰富的GUI界面。\n官方文档中的布局列表查看：Layout List。\n更多控件 # 前面的演示案例中，用到了两个控件：Label 和 Button，Fyne 还支持其他多种控件，它们都为于 widget 包中。\n我尝试在一份代码中展示出来，如下是常见控件一览：\n// 标签 Label label := widget.NewLabel(\u0026#34;Label\u0026#34;) // 按钮 Button button := widget.NewButton(\u0026#34;Button\u0026#34;, func() {}) // 输入框 Entry entry := widget.NewEntry() entry.SetPlaceHolder(\u0026#34;Entry\u0026#34;) // 复选框 Check check := widget.NewCheck(\u0026#34;Check\u0026#34;, func(bool) {}) // 单选框 Check radio := widget.NewRadioGroup([]string{\u0026#34;Option 1\u0026#34;, \u0026#34;Option 2\u0026#34;}, func(string) {}) // 选择框 selectEntry := widget.NewSelectEntry([]string{\u0026#34;Option A\u0026#34;, \u0026#34;Option B\u0026#34;} // 进度条 progressBar := widget.NewProgressBar() // 滑块 slider := widget.NewSlider(0, 100) // 组合框 combo := widget.NewSelect([]string{\u0026#34;Option A\u0026#34;, \u0026#34;Option B\u0026#34;, \u0026#34;Option C\u0026#34;}, func(string) {}) // 表单项 formItem := widget.NewFormItem(\u0026#34;FormItem\u0026#34;, widget.NewEntry()) form := widget.NewForm(formItem) // 手风琴 accordion := widget.NewAccordion(widget.NewAccordionItem(\u0026#34;Accordion\u0026#34;, widget.NewLabel(\u0026#34;Content\u0026#34;))) // Tab 选择 tabs := container.NewAppTabs( container.NewTabItem(\u0026#34;Tab 1\u0026#34;, widget.NewLabel(\u0026#34;Content 1\u0026#34;)), container.NewTabItem(\u0026#34;Tab 2\u0026#34;, widget.NewLabel(\u0026#34;Content 2\u0026#34;)), ) // 弹出对话框示例按钮 dialogButton := widget.NewButton(\u0026#34;Show Dialog\u0026#34;, func() { dialog.ShowInformation(\u0026#34;Dialog\u0026#34;, \u0026#34;Dialog Content\u0026#34;, w) }) // 滚动布局 content := container.NewVScroll(container.NewVBox( label, button, entry, check, radio, selectEntry, progressBar, slider, combo, form, accordion, tabs, dialogButton, )) w.SetContent(content) 演示效果：\nFyne 中的自定义 # 如果在实际项目中使用 Fyne，基本上是要使用 Fyne 的自定义能力。Fyne 提供了自定义控件、布局和主题等。\n自定义控件 # fyne 是支持实现自定义控件的，这涉及定义控件的绘制方法和布局逻辑。我们主要是实现两个接口：fyne.Widget 和 fyne.WidgetRenderer。\nfyne.Widget 的定义如下所示：\ntype Widget interface { CanvasObject CreateRenderer() WidgetRenderer } CreateRenderer 方法返回的就是 WiddgetRenderer，用于定义控件渲染和布局的逻辑。\ntype WidgetRenderer interface { Destroy() Layout(Size) MinSize() Size Objects() []CanvasObject Refresh() } 这样拆分的目标是为将了控件的逻辑和 UI 绘制分离开来，在 Widget 中专注于逻辑，而 WidgetRenderer 中专注于渲染布局。\n假设实现一个类似 Label 的控件，类型定义：\ntype CustomLabel struct { widget.BaseWidget Text string } 它继承了 wiget.BaseWidget 基本控件实现，Text 就是要 Label 显示的文本。还要给给 CustomLabel 实现 CreateRenderer 方法。\n定义 CustomLabel 创建函数：\nfunc NewCustomLabel(text string) *CustomLabel { label := \u0026amp;CustomLabel{Text: text} label.ExtendBaseWidget(label) return label } customWidgetRenderer 类型定义如下：\ntype customWidgetRenderer struct { text *canvas.Text // 使用canvas.Text来绘制文本 label *CustomLabel } 实现 CustomLabel 的 CreateRenderer 方法。\nfunc (label *CustomLabel) CreateRenderer() fyne.WidgetRenderer { text := canvas.NewText(label.Text, theme.ForegroundColor()) text.Alignment = fyne.TextAlignCenter return \u0026amp;customLabelRenderer{ text: text, label: label, } } 构建 Renderer 变量，使用 canvas 创建 Text 文本框，为适配主题使用主题配置前景色。还有，设置文本居中显示。\n而 customLabelRenderer 要实现 WidgetRender 接口定义的所有方法。\nfunc (r *customLabelRenderer) MinSize() fyne.Size { return r.text.MinSize() } func (r *customLabelRenderer) Layout(size fyne.Size) { r.text.Resize(size) } func (r *customLabelRenderer) Refresh() { r.text.Text = r.label.Text r.text.Color = theme.ForegroundColor() // 确保文本颜色更新 r.text.Refresh() } func (r *customLabelRenderer) BackgroundColor() color.Color { return theme.BackgroundColor() } func (r *customLabelRenderer) Objects() []fyne.CanvasObject { return []fyne.CanvasObject{r.text} } func (r *customLabelRenderer) Destroy() {} 在 main 函数中，尝试使用这个控件。\na := app.New() w := a.NewWindow(\u0026#34;Custom Label\u0026#34;) w.SetContent(NewCustomLabel(\u0026#34;Hello\u0026#34;)) w.ShowAndRun() 显示的效果和 Label 控件是类似的。\n其他自定义 # 其他自定义能力，如 Layout、Theme，我就不展开介绍。如果有机会，写点实际应用案例。如果基于案例介绍，会更有体悟吧。\n还有，Fyne 的官方文档写的挺易读的，可直接看它的文档。\n数据绑定 # Fyne 从 v2.0.0 开始支持数据绑定。它让控件和与数据实时连接，数据更改会自动反映在UI上，反之亦然。\n控件为了支持数据绑定能力，一般会提供如 NewXXXWithData 的接口。直接通过场景说明，场景：编辑输入框的内容，可直接实现到 Label 上。\n核心代码如下所示：\n// 创建一个字符串绑定 textBind := binding.NewString() // 创建一个 Entry，将其内容绑定到 textBind entry := widget.NewEntryWithData(textBind) // 创建一个 Label，也将其内容绑定到同一个 textBind label := widget.NewLabelWithData(textBind) // 使用容器放置 Entry 和 Label，以便它们都显示在窗口中 content := container.NewVBox(entry, label) 显示效果，如下所示：\n尝试让前面自定义的 CustomLabel 支持数据绑定能力。只要创建一个 NewCustomLabelWithData 构造函数。\n如下所示：\nfunc NewCustomLabelWithData(data binding.String) *CustomLabel { label := \u0026amp;CustomLabel{} label.ExtendBaseWidget(label) data.AddListener(binding.NewDataListener(func() { text, _ := data.Get() label.Text = text label.Refresh() })) return label } 通过 data 监听数据变化后，立刻刷新组件。\n如上所示，我们这个自定义 Label 中的文本是居中显示的。\n数据绑定的核心是监听器模式（Observer pattern）。每个绑定对象内部维护了一个监听器列表，当数据变化时，这些监听器会被通知更新。\n在 Fyne 中，通过 data.AddListner() 将 UI 组件与数据绑定对象绑定时，实际上是在数据对象上注册了一个监听器，这个监听器会在数据变化时更新 UI 组件的状态。\n结语 # Fyne 是简单、强大和跨平台的 GUI 工具，使得用 GO 开发现代 GUI 应用多了一个优秀选择。随着对 Fyne 的深入，它能够更加灵活地构建出符合需求的应用。\n我喜欢用 Go 的原因，很重要的原因就是它的简洁性，很容易看到本质的东西，但又无需理解太复杂的编程概念。\n","date":"2024-03-07","externalUrl":null,"permalink":"/posts/2024-03-01-fyne-in-golang/","section":"文章","summary":"今天，推荐一个 Go 实现的 GUI 库 - fyne。\nGo 官方也没有提供标准的 GUI 框架，在 Go 实现的几个 GUI 库中，Fyne 算是最出色的，它有着简洁的API、支持跨平台能力，且高度可扩展。这也就是说，Fyne 是可以开发 App。\n","title":"一个 Go 实现的跨平台 GUI 框架 Fyne","type":"posts"},{"content":"之前看到 Github 有个非常受欢迎的 build-your-own-x 仓库，觉得挺有意思的，有不少有趣的实现。我就想着多尝试实现些这样的小项目，看看不同的领域。一方面提升我的编程能力，另外，也希望能发现一些不错的项目。\n今天的项目在 build-your-own-x 中也能找到，即 build your own shell。这个项目能帮助学习 Go 如何进行如 IO 输入输出、如何发起进程调用等操作。\n核心流程 # 首先，我声明这是个简陋的 shell，但能帮助我们更好理解 Shell。它支持如提示符打印、读取用户输入、解析输入内容、执行命令，另外还支持开发内建命令。\n如下是演示效果：\n接下来，我将从零开始一步步复现我的整个开发过程。\n框架搭建 # 我从创建一个 Shell 结构体开始，这是整个shell程序的核心，它其中包含一个 bufio.Reader 从标准输入读取用户输入。\ntype Shell struct { reader *bufio.Reader } func NewShell() *Shell { return \u0026amp;Shell{ reader: bufio.NewReader(os.Stdin), } } 如上，通过 NewShell 构造函数创建 Shell 实例。这个函数返回一个新的 Shell 实例，其中包含了初始化的 bufio.Reader。\n为了方便扩展，接下来添加了几个方法，分别是：\nPrintPrompt用于打印提示符； ReadInput用于读取用户输入； ParseInput用于解析输入并分割成命令名和参数； ExecuteCmd用于执行命令。 定义如下：\nfunc (s *Shell) PrintPrompt() func (s *Shell) ReadInput() (string, error) func (s *Shell) ParseInput(input string) (string, []string) func (s *Shell) ExecuteCmd(cmdName string, cmdArgs []string) error 它们就是核心流程中最重要的四个方法，都是在 RunAndListen 方法中被调用，如下所示：\nfunc (s *Shell) RunAndListen() error { for { s.PrintPrompt() input, err := s.ReadInput() if err != nil { fmt.Fprintln(os.Stderr, err) continue } cmdName, cmdArgs := s.ParseInput(input) if err := s.ExecuteCmd(cmdName, cmdArgs); err != nil { fmt.Fprintln(os.Stderr, err) continue } } } 主函数 main 的代码不复杂，如下所示：\nfunc main() { s := NewShell() _ = s.RunAndListen() } 通过 NewShell 创建 Shell 示例，调用 RunAndListen 监听用户输入即可。\n接下来，我开始介绍其中每一步的实现过程。\n打印提示符 # 首先，打印提示符的代码，非常简单，如下所示：\nfunc (s *Shell) PrintPrompt() { fmt.Print(\u0026#34;$ \u0026#34;) } 单纯的打印 $ 作为提示符，更复杂的场景可以加上路径提示，如：\n[~/demo/shell]$ 修改后的代码如下所示：\nfunc (s *Shell) PrintPrompt() { // 获取当前工作目录 cwd, err := os.Getwd() if err != nil { // 如果无法获取工作目录，打印错误并使用默认提示符 fmt.Println(\u0026#34;Error getting current directory:\u0026#34;, err) fmt.Print(\u0026#34;$ \u0026#34;) return } // 获取当前用户的HOME目录 homeDir, err := os.UserHomeDir() if err != nil { fmt.Println(\u0026#34;Error getting home directory:\u0026#34;, err) fmt.Print(\u0026#34;$ \u0026#34;) return } // 如果当前工作目录以HOME目录开头，则用\u0026#39;~\u0026#39;替换掉HOME目录部分 if strings.HasPrefix(cwd, homeDir) { cwd = strings.Replace(cwd, homeDir, \u0026#34;~\u0026#34;, 1) } // 打印包含当前工作目录的提示符 fmt.Printf(\u0026#34;[%s]$ \u0026#34;, cwd) } 这是非常粗糙的拿到目录并打印出来。\n通常 Shell 的提示符是可以自定义，有兴趣可以在这里扩展个接口类型，用于不同提示符的格式化实现。\n读取用户输入 # 最简单的读取用户输入的代码，代码如下：\nfunc (s *Shell) ReadInput() (string, error) { input, err := s.reader.ReadString(\u0026#39;\\n\u0026#39;) if err != nil { return \u0026#34;\u0026#34;, err } return input, nil } 按 \\n 分割命令，分割出来的文本可以理解为一次执行请求。\n但实际情况是在使用 Shell 时，我们会发现一些特殊符号是要处理，如引号。\n例如：\n[~/demo/shell]$ echo \u0026#39; Hello World! Nice to See you! \u0026#39; 下面是一个简化的实现：\nfunc (s *Shell) ReadInput() (string, error) { var input []rune var inSingleQuote, inDoubleQuote bool for { r, _, err := s.reader.ReadRune() if err != nil { return \u0026#34;\u0026#34;, err } // Check for quote toggle switch r { case \u0026#39;\\\u0026#39;\u0026#39;: inSingleQuote = !inSingleQuote case \u0026#39;\u0026#34;\u0026#39;: inDoubleQuote = !inDoubleQuote } // Break on newline if not in quotes if r == \u0026#39;\\n\u0026#39; \u0026amp;\u0026amp; !inSingleQuote \u0026amp;\u0026amp; !inDoubleQuote { break } input = append(input, r) } return string(input), nil } 如上的代码中，逐一读取输入内容。程序中，通过判断当前是处于引号中，保证正确识别用户输入。\n如果你读过我之前一篇文章，熟练使用 bufio.Scanner 类型，也可以用它提供的自定义分割规则的方式，在这个场景下也可以使用。我的完整源码 goshell 就是基于 Scanner 实现的。\n另外，这个输入不支持删除，如果我输出错了，只能退出重来，也是挺头疼的。如果要实现，要依赖于其他库实现。\n解析输入 # 读取完成，通过 ParseInput 方法解析成 cmdName 和 cmdArgs，代码如下：\nfunc (s *Shell) ParseInput(input string) (string, []string) { input = strings.TrimSuffix(input, \u0026#34;\\n\u0026#34;) input = strings.TrimSuffix(input, \u0026#34;\\r\u0026#34;) args := strings.Split(input, \u0026#34; \u0026#34;) return args[0], args[1:] } 真正的 Shell 肯定比这个强大的多了。最容易想到的，一次 shell 执行请求可能包含多个命令，甚至是 shell 脚本。\n太复杂的能力实现起来太麻烦，我们可以支持一个最简单的能力，分号分割运行多个命令。\n$ cd /; ls 我们修改代码，支持这个能力。\ntype CmdRequest struct { Name string Args []string } func (s *Shell) ParseInput(input string) []*CmdRequest { subInputs := strings.Split(input, \u0026#34;;\u0026#34;) cmdRequests := make([]*CmdRequest, 0, len(subInputs)) for _, subInput := range subInputs { subInput = strings.Trim(subInput, \u0026#34; \u0026#34;) subInput = strings.TrimSuffix(subInput, \u0026#34;\\n\u0026#34;) subInput = strings.TrimSuffix(subInput, \u0026#34;\\r\u0026#34;) args := strings.Split(subInput, \u0026#34; \u0026#34;) cmdRequests = append(cmdRequests, \u0026amp;CmdRequest{Name: args[0], Args: args[1:]}) } return cmdRequests } 上面代码里，定义了一个新类型 CmdRequest，它用于保存从用户输入解析而来的命令名和命令参数。\n由于修改了 ParseInput 的返回类型，RunAndListen 中的逻辑就要改动了。\n如下所示：\nfor { // ... cmdRequests := s.ParseInput(input) for _, cmdRequest := range cmdRequests { cmdName := cmdRequest.Name cmdArgs := cmdRequest.Args if err := s.ExecuteCmd(cmdName, cmdArgs); err != nil { fmt.Fprintln(os.Stderr, err) continue } } } 到此，通过分号分割多命令也是支持的了。\n命令执行 # 最后一步就是执行命令了。代码如下所示：\nfunc (s *Shell) ExecuteCmd(cmdName string, cmdArgs []string) error { cmd := exec.Command(cmdName, cmdArgs...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout return cmd.Run() } 我使用的是标准库 exec 包中的 Command 类型创建一个命令用于执行外部命令。\n这个命令的标准输出和标准错误都被设置为当前进程的对应输出，这样命令的输出就可以直接显示给用户。\n最后，通过调用 cmd.Run() 执行该命令即可。\n退出功能 # 在初步测试中，我发现 shell 还不支持退出。为了解决这个问题，我在 RunAndListen 循环中加入了对 exit 命令的检查。\nfor { cmdName := cmdRequest.Name cmdArgs := cmdRequest.Args if cmdName == \u0026#34;exit\u0026#34; { return nil } if err := s.ExecuteCmd(cmdName, cmdArgs); err != nil { } } 如果用户输入的是exit，循环将终止，直接退出shell。\n内建命令 # 现在，如果测试这个代码，看起来运转一切正常。但如果仔细测试，会发现它还不支持 cd 的能力。\n为什么 cd 不能用？\n因为改变当前目录要修改进程的工作目录，这种操作不能像其他外部命令那样通过创建新进程实现。因此，我引入了内建命令的实现，并实现第一个内建命令了ChangeDirCommand。\n首先是搭建一个简单框架，定义一个接口：\ntype BuiltinCmder interface { Execute(args ...string) error } 任何实现了这个接口的类型都可以作为内建的命令。\n在 Shell 类型新建了一个字段，名为 builtinCmds ，修改定义如下：\ntype Shell struct { reader *bufio.Reader builtinCmds map[string]BuiltinCmder } 并添加了一个方法，名为 RegisterBuiltinCmd：\nfunc (s *Shell) RegisterBuiltinCmd(cmdName string, cmd BuiltinCmder) { s.builtinCmds[cmdName] = cmd } 在 Shell 的 ExecuteCmd 中新增了内建命令的执行：\nfunc (s *Shell) ExecuteCmd(cmdName string, cmdArgs []string) error { if cmd, ok := s.builtinCmds[cmdName]; ok { return cmd.Execute(cmdArgs...) } cmd := exec.Command(cmdName, cmdArgs...) // ... } 现在，只要实现 ChangeDirCommand，并在 main 入口函数注册这个内建就行了。ChangeDirCommand 代码和入口注册代码，如下所示：\ntype ChangeDirCommand struct{} func (c *ChangeDirCommand) Execute(args ...string) error { if len(args) \u0026lt; 2 { return errors.New(\u0026#34;Expected path argument\u0026#34;) } return os.Chdir(args[1]) } func main() { s := NewShell() s.RegisterBuiltinCmd(\u0026#34;cd\u0026#34;, \u0026amp;ChangeDirCommand{}) _ = s.RunAndListen() } 到此大功搞成，源码地址：goshell\n总结 # 通过开发这个简单的 shell，了解 Go 如何读取如用户输入，解析与执行用户命令。对 shell 的流程也有了一个大概了解。\n未来，如果有想法，或许会继续扩展这个 shell，添加更多内建命令，可以将不同部分模块化，如 Prompt, Reader, Parser 和 Command 都是可以继续抽象以支持更多能力。\n如果继续它的开发，期待学习到更多关于系统编程和 Go 语言的高级特性。而且shell 可不止这点能力，如果你了解 shell，使用过 bash 或是 zsh 等 shell 就知道它们是如何提高我们工作效率的了。\n","date":"2024-03-01","externalUrl":null,"permalink":"/posts/2024-02-29-create-your-own-shell-in-golang/","section":"文章","summary":"之前看到 Github 有个非常受欢迎的 build-your-own-x 仓库，觉得挺有意思的，有不少有趣的实现。我就想着多尝试实现些这样的小项目，看看不同的领域。一方面提升我的编程能力，另外，也希望能发现一些不错的项目。\n","title":"我用 Go 开发了一个简易版 shell","type":"posts"},{"content":"本文是 GO 三方库推荐的第 5 篇，继续介绍数据库 schema 同步工具，我前面已经写了两篇这个主题的文章。系列查看：Golang 三方库。\n今天，推荐是的一个基于差异实现数据库 schema 迁移的工具库 - skeema，同样由 Go 实现。\n背景 # 我的上家公司时，有名架构师开发了一个类似于 skeema 的工具，也是基于差异同步表结构的，区别在于那个工具使用的 yaml 声明表结构定义的。我当时就想在市面上找一个类似的工具，但没找到。好在通过 ChatGPT，搜索效率提升很多，不过也是一步步的引导才让我找到这个工具。\n概述 # Skeema 是基于差异同步数据库 schema，这让我们只要表结构的终态就行。 还有，skeema 支持在不同环境间同步数据库 schema。\nSkeema 支持 linter 识别 SQL 语句，方便我们将其集成到 CICD 中提升 Schema 质量。还有，它默认是禁用了一些不安全的数据库操作，如删表删字段等操作。\n如果要说缺点，它现在只支持 MySQL 和 MariaDB。\n安装 # Skeema 的安装过程直接了当。如果你使用的是 MacOS，可以通过Homebrew来安装：\nbrew install skeema/tap/skeema 或是也可通过 go get 安装，但 Go 的版本要求在 v1.21+。\n$ go install github.com/skeema/skeema@v1.11.1 对于其他平台，可以从Skeema的GitHub页面下载二进制文件，并按照文档指引进行安装。\n使用 # Skeema 的使用可概括为 5 个核心步骤：\n通过 skeema init 下载初始建表 SQL； 基于初始 SQL 按需求修改文件； 使用 skeema diff 在提交前确认差异； 使用 skeema lint 检查 SQL 是否满足 linter 规则； 使用 skeema push 推送 SQL，变更数据库 schema。 我将按这个步骤展开介绍。\n初始化 # 首先，通过 skeema init 实现基于现有的数据库结构初始我们的目录，生成初始建表语句。\n示例如下：\n$ skeema init -h 127.0.0.1 -uroot -ppassword --schema blog -d . --schema 用于指定目标库名称，如果没有指定，会生成实例下所有库的建表 SQL。而 -d/--dir 用于指定目标目录。\n现在，回车确认和输入密码执行命令。\n由于我这个 blog 数据库没有任何内容，只会在当前目录下生成一个 .skeema 文件，它也就是 skeema 的配置文件。\n打开它，查看内容如下：\ndefault-character-set=utf8mb4 default-collation=utf8mb4_0900_ai_ci generator=skeema:1.11.1-community schema=blog [production] flavor=mysql:8.3 host=127.0.0.1 port=3306 user=root 如果想在接下来执行其他命令时，省略掉 -p 输入密码的步骤，可加上密码配置。\n... port=3306 user=root password=password 这一步会在指定目录下创建数据库架构的本地表示。\n而如果我没有指定 --schema 选项，将会把数据库中所有库表初始化到我的目录下。\n$ ls -a .skeema article blog core 生成差异 # 现在尝试做一些变更，article 库下的有一个名为 comments 的表，它的建表语句如下所示：\nCREATE TABLE `comments` ( `id` int NOT NULL, `post_id` int NOT NULL, `author_name` int NOT NULL, `comment` text NOT NULL, `update_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 我将在其中添加一个 author_id 字段，修改的建表语句如下所示。\nCREATE TABLE `comments` ( ... `author_id` varchar(255) NOT NULL, ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 现在，让我们使用 skeema diff 查看差异。\n$ skeema diff 2024-02-22 18:53:09 [INFO] Generating diff of 127.0.0.1:3306 article vs /Users/jian.xue/demo/databasemigration/skeema2-demo/article/*.sql -- instance: 127.0.0.1:3306 USE `article`; ALTER TABLE `comments` ADD COLUMN `author_id` int NOT NULL AFTER `post_id`; 2024-02-22 18:53:09 [INFO] 127.0.0.1:3306 article: diff complete ... 从上得到在 comments 新增 author_id，要执行的 SQL 是：\nALTER TABLE `comments` ADD COLUMN `author_id` int NOT NULL AFTER `post_id`; 这个命令成功输出了当前目录下建表语句的结构与数据库中表结构的差异。通过查看这个差异，即可确认是否是我们预期的 SQL。\n但问题是，和代码审阅一样，如果所有问题都是依靠人来检查，则很难最大限度降低问题发生的可能性。在代码审阅的流程中，通过会加入 linter 工具，SQL 同样也可以有这流程。好在，skeema 也提供了类似这样 lint 能力。\nLinter # Skeema 内置了一个linter 工具，检查表结构定义语句，防止一些常规的问题。\n运行命令：\nskeema lint Skeema 提供有大量规则选项，有兴趣去看下它的官方文档：Option Reference。\n如下是我列举一些常见的大家感兴趣的规则：\n# 设置默认的字符集，推荐使用 utf8mb4 以支持全字符集 default-character-set=utf8mb4 # 确保所有表和列使用推荐的字符集 lint-charset=error allow-charset=utf8mb4 # 确保所有表使用推荐的存储引擎，如 InnoDB lint-engine=error allow-engine=innodb # 推荐使用合适的数据类型作为主键，通常是整数类型 lint-pk-type=error allow-pk-type=int,bigint # 根据团队策略对外键使用进行限制，如果避免使用外键以提升性能 lint-has-fk=warning # 确保自增列使用合适的数据类型，避免未来可能的整数溢出 lint-auto-inc=error # 避免重复或冗余的索引 lint-dupe-index=error # 如果应用策略限制了存储过程和函数的使用 lint-has-routine=warning # 确保表使用一致的命名规则，如全部小写 lint-name-case=error 现在尝试对 comments 做一些变更，测试能检查出主键类型错误和索引问题。\nCREATE TABLE `comments` ( `id` CHAR(32) NOT NULL, `post_id` int NOT NULL, `author_name` int NOT NULL, `comment` text NOT NULL, `update_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_post_id` (`post_id`), KEY `idx_post_id_duplicate` (`post_id`), -- 重复的索引，与 idx_post_id 完全相同 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 执行如下命令：\n$ skeema lint 2024-02-23 15:55:55 [INFO] Linting /xxxx/skeema-demo 2024-02-23 15:55:55 [INFO] Linting /xxxx/skeema-demo/article 2024-02-23 15:55:55 [INFO] Wrote /xxxx/skeema-demo/article/comments.sql (500 bytes) 2024-02-23 15:55:55 [ERROR] /xxxx/skeema-demo/article/comments.sql:2: Column id of table `comments` is using data type char(32), which is not configured to be permitted in a primary key. The following data types are listed in option allow-pk-type: int. 2024-02-23 15:55:55 [ERROR] /xxxx/skeema-demo/article/comments.sql:10: Indexes idx_post_id_duplicate and idx_post_id of table `comments` are functionally identical. One of them should be dropped. Redundant indexes waste disk space, and harm write performance. 2024-02-23 15:55:55 [ERROR] Found 2 errors 提示索引类型错误：\nColumn id of table `comments` is using data type char(32), which is not configured to be permitted in a primary key. The following data types are listed in option allow-pk-type: int. 提示重复索引错误：\nIndexes idx_post_id_duplicate and idx_post_id of table `comments` are functionally identical. One of them should be dropped. Redundant indexes waste disk space, and harm write performance. skeema 有社区版和商业版，有一些能力检查规则要商业版支持。\n应用变更 # skeema 使用的最后一步是应用这些变更，使用 skeema push 命令即可将更新应用到数据库。这个没啥可介绍的了。\n多环境配置 # skeema 支持多环境的不同配置，上面的配置实例中其实已经能看出了。其中有一个 production 段。\ndefault-character-set=utf8mb4 default-collation=utf8mb4_0900_ai_ci generator=skeema:1.11.1-community schema=blog [production] flavor=mysql:8.3 host=127.0.0.1 port=3306 user=root 如我增加一个 dev 环境，命令如下：\n$ skeema add-environment dev --host 127.0.0.1 -uroot -ppassword 配置如下：\n[dev] flavor=mysql:8.3 host=127.0.0.1 port=3306 user=root password=password 使用时，只要在命令后加上环境名称即可，如：\n$ skeema push dev 还有，在不同环境下使用不同 lint 规则，这也是可以的。\n安全操作 # 还有一定要说明，记住 skeema 默认是不允许一些非安全的操作，如删表、列其它如修改导致数据截断啥的等等操作。这也是很符合实际场景的。不然，因为某操作，导致删掉一些数据就完犊子了。\n如果有非安全操作的需求，如在开发环境，不喜欢保留一些开发期间临时创建的表，配置 allow-unsafe=true 即可。\n[dev] ... allow-unsafe=true 总结 # Skeema 作为一个基于差异同步数据库 schema 的工具，简单强大。它简化了数据库 schema 的管理。使得我们无论是开发新功能时管理架构变更，还是在多环境中同步数据库架构，Skeema都能提供有效的支持，都不再复杂。\n最后，如果你和我曾经一样，为不同环境间的数据库表结构管理同步感到头痛，试试 Skeema，或许你会喜欢这种方式。\n","date":"2024-02-26","externalUrl":null,"permalink":"/posts/2024-02-28-database-migration-tools-skeema-in-golang/","section":"文章","summary":"本文是 GO 三方库推荐的第 5 篇，继续介绍数据库 schema 同步工具，我前面已经写了两篇这个主题的文章。系列查看：Golang 三方库。\n今天，推荐是的一个基于差异实现数据库 schema 迁移的工具库 - skeema，同样由 Go 实现。\n","title":"一个基于差异同步数据库结构的工具 - Skeema","type":"posts"},{"content":"嗨！大家好，我是波罗学。本文是 Golang 三方库推荐第四篇，系列查看：Golang 三方库。\n上篇文章，我讨论了数据库 schema 同步的两种方式：增量和差异。今天，推荐一个基于 Go 实现的增量同步数据库 schema 的工具库 - goose。我不知道其他人的情况，我工作的很长一段时间内，接触到的这类工具都是采用增量方式实现的。\n让我们开始正文吧！\n为什么选择 Goose？ # 首先，市场上已经存在了那么多数据库迁移工具，如 Flyway、Liquibase 和 Alembic。它们一般也都是采用增量方式管理数据库迁移。\n在这众多选项中，为什么要用 Goose 呢？我觉得最主要原因就是，它简单易上手，我们能迅速掌握并开始迁移任务。\n快速一览 Goose 功能特性 # 多数据库支持：Goose 可与多种数据库系统一起使用，包括但不限于 MySQL, PostgreSQL, SQLite, 和SQL Server。\n向前和向后迁移：支持向前（up）和向后（down）迁移，不仅可以通过迁移来更新数据库结构，还可以回滚到之前的状态。\n编程语言灵活性：Goose 最初是用 Go 编写的，但它支持在迁移文件中使用纯 SQL，这就拓宽了它的目标用户群体。\n命令行接口：Goose 提供了一个简单的命令行接口（CLI），使执行迁移操作更简单直观。这篇文章中的演示都是基于命令接口。\n版本控制：Goose 的每次迁移都会记录版本号，使数据库的版本控制变得清晰。版本号是基于时间生成的。\n环境配置：Goose 本身是不支持的，我简单改造了下，，基于它写了 shell 脚本，让它支持根据不同的环境（如开发、测试、生产）配置和应用不同配置，实现不同迁移策略。\n安装 # 安装 Goose 相当简单。首先，确保你已经安装了 Go。然后，通过下面命令安装 Goose：\n$ go install github.com/pressly/goose/v3/cmd/goose@latest 这条命令会将 goose 命令安装到 GOBIN 目录下。\n或者 MacOS：\n$ brew install goose 到此，就顺利安装成功了。\n演示案例 # 接下来，我将通过案例逐步演绎 Goose 的使用，基于 Goose cli 命令管理这些 SQL 脚本。\n创建迁移 # 假设，现在要为一个项目添加一个用户表。第一步就是创建一个 SQL 迁移脚本。\n迁移脚本创建，命令如下：\n$ goose create create_users_table sql 2024/02/20 17:52:58 Created new file: 20240220095258_create_users_table.sql 命令的最后一个参数 sql 表示创建的 SQL 脚本，如果没有这个参数，默认创建的是 Go 脚本。\n默认脚本内容：\n-- +goose Up -- +goose StatementBegin SELECT \u0026#39;up SQL query\u0026#39;; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin SELECT \u0026#39;down SQL query\u0026#39;; -- +goose StatementEnd 这个示例脚本包含了两部分：同步（Up）和回滚（Down）。其中的 - +goose Up 标记了接下来的 SQL 语句是用于同步迁移的。而 -- +goose Down 标记了接下来的 SQL 语句是用于回滚的。\n我们用创建 users 表的 SQL 替换其中的 SELECT 'up SQL query，用删除 users 表的 SQL 替换 SELECT 'down SQL query：\n-- +goose Up -- +goose StatementBegin CREATE TABLE users ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL, password CHAR(32) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL ); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP TABLE if EXISTS users; -- +goose StatementEnd 我们使用 Goose 的 up 执行同步迁移操作。\n$ goose mysql \u0026#34;root:password@/core?parseTime=true\u0026#34; up 2024/02/20 18:07:57 OK 20240220095258_create_users_table.sql (21.8ms) 2024/02/20 18:07:57 goose: successfully migrated database to version: 20240220095258 这条命令会应用所有未执行的迁移脚本，创建数据库 users 表结构。Goose 会跟踪每次迁移的状态，确保每个迁移只被执行一次。\n如果打开数据库，查看表的创建结果，你会发现，数据库中多了一个非我们预期内的表 - goose_db_version。这个表就是 goose 用来追踪每次迁移的状态信息。\nmysql\u0026gt; show tables; +------------------+ | Tables_in_core | +------------------+ | goose_db_version | | users | +------------------+ 2 rows in set (0.01 sec) mysql\u0026gt; select * from goose_db_version; +----+----------------+------------+---------------------+ | id | version_id | is_applied | tstamp | +----+----------------+------------+---------------------+ | 1 | 0 | 1 | 2024-02-20 10:07:57 | | 2 | 20240220095258 | 1 | 2024-02-20 10:07:57 | +----+----------------+------------+---------------------+ 如果想回滚之前创建的表，使用命令 down 即可回滚。\n$goose mysql \u0026#34;root:password@/core?parseTime=true\u0026#34; down 2024/02/20 18:21:38 OK 20240220095258_create_users_table.sql (16.7ms) Goose 其他命令一览 # Goose 其他的一些命令如下所示。当使用 Goose 进行数据库迁移时，你可以使用它们。\nCommands: up-by-one Migrate the DB up by 1 up-to VERSION Migrate the DB to a specific VERSION down-to VERSION Roll back to a specific VERSION redo Re-run the latest migration reset Roll back all migrations status Dump the migration status for the current DB version Print the current version of the database fix Apply sequential ordering to migrations validate Check migration files without running them 快速一览：\nup-by-one： 逐步迁移数据库，只迁移一个版本。 up-to VERSION： 迁移数据库到指定的版本。 down-to VERSION： 回滚数据库到指定的版本。 redo： 重新运行最新的迁移。 reset： 回滚所有的迁移，删除所有已应用的迁移。 status： 输出当前数据库迁移的状态。它会显示已生效版本和未生效版本。 version： 打印当前数据库的版本号。 fix： 将迁移文件转换为顺序号顺序。确保按照顺序号的执行。 validate： 检查迁移文件的有效性，但不运行它们。 无版本控制迁移 # Goose 除支持版本控制管理迁移脚本，我们还可以完全去掉版本，传递选项 --no-versioning 给命令即可。\n为什么要这个能力？官方文章中提供的使用场景，如某些情况下，我们只是想插入一些数据，或者开发测试时修正一些问题，就可以通过方式处理。\n. └── schema ├── migrations │ ├── 00001_add_users_table.sql │ ├── 00002_add_posts_table.sql │ └── 00003_alter_users_table.sql └── seed ├── 00001_seed_users_table.sql └── 00002_seed_posts_table.sql 将要版本控制的放在 migrations 目录下，而无版本控制的插入数据的脚本存放在 seed 目录。\n演示命令，如下所示：\ngoose -dir ./schema/migrations up goose -dir ./schema/seed -no-versioning up 管理多环境 # 前面的演示，总是要在命令行上指定数据库类型和连接参数才能连接真实环境数据库，即麻烦又容易出错。\n$ goose mysql \u0026#34;root:password@/core?parseTime=true\u0026#34; up 好在，Goose 提供了通过环境变量配置数据库的连接信息。\n# 数据库类型，如 MySQL、PostgreSQL 等 GOOSE_DRIVER=DRIVER # 连接信息，如 MySQL root:password@/core?parseTime=true GOOSE_DBSTRING=DBSTRING 现在就可以直接执行如 goose up、goose down 等命令。\n另外，我为了让 Goose 支持多环境配置，开发了一个简单的 shell 脚本，从配置中获取环境信息，设置到这两个环境变量上。\n配置的形式如下：\ndev: driver: mysql open: user:password@/dbname?parseTime=true test: driver: mysql open: user:password@/test_dbname?parseTime=true prod: driver: mysql open: user:password@/prod_dbname?parseTime=true 在上面的例子中，我们定义了三个环境：dev、test 和 prod。每个环境都有自己的数据库连接字符串。\n支持环境管理的命令脚本源码如下所示，我将其命名为 goosem。\n#!/bin/bash assert_installed_cmd() { if ! command -v $1 \u0026amp;\u0026gt; /dev/null then echo -e \u0026#34;$1 could not be found. Please install $1:\\n\\t$2\u0026#34; exit 1 fi } assert_installed_cmd yq \u0026#34;yq\u0026#34; \u0026#34;brew install yq\u0026#34; assert_installed_cmd goose \u0026#34;goose\u0026#34; \u0026#34;brew install goose\u0026#34; if [ \u0026#34;$#\u0026#34; -lt 1 ]; then echo \u0026#34;Usage: $0 \u0026lt;environment\u0026gt; [goose commands]\u0026#34; exit 1 fi ENV=$1 CONFIG_FILE=\u0026#34;dbconf.yml\u0026#34; DRIVER=$(yq e \u0026#34;.$ENV.driver\u0026#34; $CONFIG_FILE) DBSTRING=$(yq e \u0026#34;.$ENV.open\u0026#34; $CONFIG_FILE) if [ \u0026#34;$DRIVER\u0026#34; == \u0026#34;null\u0026#34; ] || [ \u0026#34;$DBSTRING\u0026#34; == \u0026#34;null\u0026#34; ]; then echo \u0026#34;Configuration for environment \u0026#39;$ENV\u0026#39; not found.\u0026#34; exit 1 fi export GOOSE_ENV=$ENV export GOOSE_DRIVER=$DRIVER export GOOSE_DBSTRING=$DBSTRING shift goose \u0026#34;$@\u0026#34; 现在，我们可以通过如下命令使用 Goose 了。\ngoosem dev up 这个脚本里面依赖了一个 yaml 解析工具，名为 yq，要提前安装下：\n$ brew install yq 现在，Goose 就有了支持多环境的能力了。非常简单。\n如果你有按环境执行不同的迁移脚本的需求，可通过使用 Go 迁移脚本，在其中添加环境检查的逻辑来实现这一点。\n举例来说，你可以在 Go 迁移脚本中检查是否是 prod 环境决定是否执行某部分迁移逻辑。\nif os.Getenv(\u0026#34;GOOSE_ENV\u0026#34;) == \u0026#34;prod\u0026#34; { // ... } 这有个前提，你要熟悉 GO 这门编程语言。\n最后 # 本文介绍了一个 GO 开发的基于增量实现数据结构迁移的工具 goose，还基于 shell 让它能进行多环境管理，有趣的，哈哈。本文主要是通过 SQL 脚本演示，Goose 本身也是支持 Go 脚本管理 Schema 的，灵活度更高。\n下篇文章，我将介绍另一个基于差异管理数据库 schema 的工具，依然是用 Go 实现的。\n","date":"2024-02-25","externalUrl":null,"permalink":"/posts/2024-02-27-database-migration-tools-goose-in-golang/","section":"文章","summary":"嗨！大家好，我是波罗学。本文是 Golang 三方库推荐第四篇，系列查看：Golang 三方库。\n上篇文章，我讨论了数据库 schema 同步的两种方式：增量和差异。今天，推荐一个基于 Go 实现的增量同步数据库 schema 的工具库 - goose。我不知道其他人的情况，我工作的很长一段时间内，接触到的这类工具都是采用增量方式实现的。\n","title":"一个基于增量同步数据库结构的工具 - Goose","type":"posts"},{"content":"我在思考如何提高终端工作效率时，想到了是否能给予 iTerm2 实现一个类似于 tmuxifier 布局管理工具。如果你不了解 tmuxifier，简单来说，它是 tmux 的布局管理工具。\n它的作用如下所示：\n想到不如做到！\n这篇文章记录了我从零开始，一步步构建这个工具的过程。\n设计阶段 # 正式开发这个工具前，先调研下可行性还是很重要的。\niTerm2 支持 Python API # 首先，我了解到 iTerm2 支持通过 Python 脚本控制终端，这为我开发这样一个工具提供了可能性。\n它提供大量的 API，如更换背景、创建窗格，切换 Tab 等都是支持的。具体可查看文档：Python-API。\n设置开发环境 # 为了顺利开始，首先确保我的 iTerm2 （3.4.23）支持 Python API，还有我的机器上配置了 Python 环境。\n接着，我安装了 iTerm2 的 Python API 所需的库。\n$ pip install iterm2 配置文件的设计 # 我选择用 YAML 作为配置文件的格式，因为它的可读性好，格式简洁，有现成的解析库。我要做的就是通过 YAML 文件要定义布局，包括窗口、窗格和命令。\n这是 tmuxifier 的配置文件。\n# 设置窗口名称和根目录 session_root \u0026#34;~/Projects/my-project\u0026#34; window_name \u0026#34;my-project\u0026#34; # 创建一个新窗口，并分割成两个窗格 new_window \u0026#34;my-project\u0026#34; split_h 50 # 第一个窗格运行 vim run_cmd \u0026#34;vim\u0026#34; 0 # 第二个窗格运行 go run main.go run_cmd \u0026#34;go run main.go\u0026#34; 1 # 设置主窗格 select_pane 0 我将其映射成 YAML 配置文件，如下所示：\nlayout: window_root: \u0026#34;~/Projects/my-project\u0026#34; window_name: \u0026#34;my-project\u0026#34; panes: - percentage: 50 commands: - \u0026#34;vim\u0026#34; - type: \u0026#34;split_h\u0026#34; commands: - \u0026#34;git status\u0026#34; main_pane: 0 这个配置旨在创建一个布局，其中包含两个窗格，一个运行 vim，另一个显示 go run main.go，而鼠标聚焦在 vim 窗格下。\n配置 yaml 文件的解析，我使用 Python 的 yaml 库来解析配置文件，\nimport yaml # 解析 YAML 配置 with open(\u0026#39;layout.yaml\u0026#39;, \u0026#39;r\u0026#39;) as f: config = yaml.safe_load(f) layout = config[\u0026#34;layout\u0026#34;] 这样我就能得到一个 layout 字典，其中包含了所有的布局信息。\n接下来，主要就是将这个配置转换为 iTerm2 的操作，也就是根据配置指示，创建窗口和窗格，以及在窗格中执行特定命令。\n实现布局管理工具 # 首先，创建一个 Python 脚本，假设就叫 itermifier 吧。这个脚本根据我的 YAML 配置创建窗格并执行命令。\n准备部分 # 在开始构建我的 iTerm2 布局管理工具前，还需要准备一些基本的变量，这些变量将在创建布局的过程中被频繁使用。\n首先，基于 iTerm2 API 创建主程序入口和获取 iTerm2 应用实例，如下：\nasync def create_layout(connection): app = iterm2.async_get_app(connection) iterm2.run_until_complete(create_layout) 代码中，定义了一个名为 create_layout 的异步函数，它是整个工具的核心。connection 参数是与 iTerm2 应用程序的连接，它是执行所有 iTerm2 API 调用的必要条件。iTerm2 的应用实例 app 就是从它获取而来的。\n这是后续所有操作的起点。\n窗口和标签页 # 接着，获取到当前活跃的窗口和标签页：\nwindow = app.current_terminal_window tab = window.current_tab 这些步骤确保了我能在正确的上下文中操作 iTerm2，无论是分割窗格还是运行命令。\n接下是创建了一个名为 sessions 的列表，用于存储标签页中的会话。\n# 预先创建的会话列表 sessions = [tab.current_session] 这个列表用于存储当前这个标签页的所有会话，初始内容中的是当前标签页会话，通过 tab.current_session 获取。\n分割窗格 # 接下来，就是按配置分割窗格。通过 for 循环窗格配置。\nfor index, pane in enumerate(layout[\u0026#34;panes\u0026#34;]): pass 以下是分割窗格的实现代码：\nvertical = False if pane[\u0026#34;type\u0026#34;] == \u0026#34;split_h\u0026#34; else True session = await tab.current_session.async_split_pane(vertical=vertical) session = sessions.append(session) 对于每个窗格配置，我调用 iTerm2 API 的 async_split_pane 方法，根据需要进行水平或垂直分割。\n执行预设命令 # 每个窗格创建后，通过 async_send_text 方法执行各个 pane 的预设命令。\nfor cmd in pane.get(\u0026#34;commands\u0026#34;, []): await session.async_send_text(f\u0026#34;{cmd}\\n\u0026#34;) # pyright: ignore 到这里，基本上已经实现了我想要的所有能力了。\n回到主窗格 # 最后，我实现了将焦点移回主窗格的功能。\n前面通过维护一个 session 列表，保存了所有创建的窗格会话，这样，我可以在所有操作完成后，按 main_pane 获取最终聚焦的窗口。\nmain_pane_index = layout[\u0026#34;main_pane\u0026#34;] await sessions[main_pane_index].async_activate() 到此，基本全部完成了。\n效果如下：\n这里面，我省略一些代码，完成代码查看这个地址：itermifier。\n结语 # 通过这个过程，我成功实现了一个在 iTerm2 中自动管理布局的工具。虽然过程中遇到了一些挑战，但最终能够通过编程方式定义和启动我的工作环境，还是很爽的。\n要说遗憾的话，就是不支持大小比例的调整，可惜了。\n","date":"2024-02-25","externalUrl":null,"permalink":"/posts/2024-02-25-build-an-itermifier/","section":"文章","summary":"我在思考如何提高终端工作效率时，想到了是否能给予 iTerm2 实现一个类似于 tmuxifier 布局管理工具。如果你不了解 tmuxifier，简单来说，它是 tmux 的布局管理工具。\n","title":"我用 Python 为 iTerm2 开发一个类似 tmuxifier 的工具","type":"posts"},{"content":"本文是关于我如何在 binance 无风险套利实现每天挣 20-25 美金的。\n不谈本金只谈收入就是耍流氓，我的本金投入大概是 33k 美金，其实算起来年化不算高，如果考虑复利且策略一直有效的话，年化也就 30% 多。\n背景 # 我做自由职业主要是两个方向：一个是自媒体方向，分享个人技术和交易领域的知识和见解。另一个是交易，通过多策略从市场套利，争取相对稳定的收益，能让我的写作生涯能坚持下去。\n我从 1 月份开始想在 medium 上挣钱，写的基本针对 Go 语言的技术文，连续写了快两个月了，现在每天收入也就是 0.5-1.5 美金。\n我就想着偶尔写点别的主题，看看是否能刺激下我的收入。我的梦想是成为一名自由者，如果只是固定思维不做改变，估计目标是很难实现的。\n进入主题，谈谈我的套利策略吧。\n策略 # 今天，我先讲讲第一个我在 binance 做的套利策略 - 二重资金费率套利。这是我的其中一个策略。如果你是一名热衷于策略交易的人，可能多少是了解什么是资金费率套利。区别是，我在策略做了一点点升级。\n简单来说：我通过同时做空 BTCUSDT 和 ETHBTC 这两个永续合约，提升了我的套利收益。\nETHBTC 是binance 去年 4 月份开始支持的永续合约，这也是为什么单独强调在 binance 而不是其其他平台执行这个策略。\n首先，什么是资金费率套利呢？\n资金费率套利是一种在加密货币市场利用永续合约的资金费率差异来赚取无风险利润的策略。我们在资金费率为正时做空，并在资金费率为负时做多，实现买低卖高。\n永续合约是加密货币衍生出了一种特别合约，通过资金费率这种实现锚定现货价格的目标。我就不更加深入的展开了。简单理解，就是如果市场做多多于做空的人，多方需要支付费率给空方。一般交易所是 8 小时结算一次，也就是一天结算 3 次。\n从去年 4 月份 biannce 的 ETHBTC 上线后，我一直在观察它，希望通过这种币币合约找到新的交易策略。而它能否资金费率套利，也是我的一个方向，从 2023 年 10 开始，它经过一段时间的下跌，我发现它开始出现资金费率，普遍是 \u0026gt;= 0。我就在我原本的 BTCUDT 套利策略上，加上了 ETHBTC 套利。\n这个策略的执行有个前提，要将账户改为 binance 的多币种交易模式，即升级为统一保证金账户，才能最大化收益。\n如果操作呢？\n假设，我手里中 25000 美金 USDT，此时的 BTC 价格是 25000 美金，ETH 价格为 0.05 BTC。\n三个步骤：\n首先，清空当前所谓仓位，修改账户模式为 mutiple asset mode；\n然后，我将持有的 25000 USDT 转为 1 BTC 现货。同时做空 1BTC 的 BTCUSDT 永续合约。每 8 小时的资金费率通常为 0.01%（大约 2.5 美金，一天 3 次 3x2.5 = 7.5 美金）。\n最后，我将持有的 1 BTC 转为 20 ETH 现货，同时做空 20 ETH 的 ETHBTC 永续合约。这个资金费率不是那么固定，这几月大概也在，0.008%，也就是 1btc x 0.008% = 0.00006 x 3 = 0.00024 BTC，按 BTC 是 40000 USDT 的价格计算，大概也是 7 美金。\n两者合并到一起，在本金 25000 的情况下，我在 25000 左右入场的，大概每天收入 15 美金。对于币圈而言，这笔收入其实也不算高，但是图个安心。\n另外 # 由于这策略风险的非常低，我用的是统一保证账户，我在这个策略上面还套了一点仓位的网格策略，在大跌时，会暴露一点多头仓位。我在 10 月份的 32000 本金，现在已经到了 45000 美金，其中有 2000 美金是套利收益。\n","date":"2024-02-24","externalUrl":null,"permalink":"/posts/2024-02-24-earn-20dollar-every-day-in-binance/","section":"文章","summary":"本文是关于我如何在 binance 无风险套利实现每天挣 20-25 美金的。\n不谈本金只谈收入就是耍流氓，我的本金投入大概是 33k 美金，其实算起来年化不算高，如果考虑复利且策略一直有效的话，年化也就 30% 多。\n","title":"我是如何每天在 binance 套利 20 美金的？","type":"posts"},{"content":"嗨，大家好！我是波罗学。本文是系列文章 Go 技巧第十九篇，系列文章查看：Go 语言技巧。\n在使用 Go 开发项目时，估计有不少人遇到过无法正确处理文件路径的问题，特别是刚从如 PHP、python 这类动态语言转向 Go 的朋友，已经习惯了通过相对源码文件找到其他文件。这个问题能否合理解决，不仅关系到程序的可移植性，还直接影响到程序的稳定性和安全性。\n本文将尝试从简单到复杂，详细介绍 Go 中获取路径的不同方法及应用场景。\n引言 # 首先，为什么要获取文件路径？\n一般来说，程序在运行时必须准确地读取相关的配置和资源以顺利启动。确定这些信息的存储位置，即获取文件路径，成为了正确访问这些信息的首要步骤，对于构建稳定可靠的应用程序而言至关重要。\n其次，为什么从动态语言转到 Go，容易被这个问题困扰？\n与 Go（一种静态语言）相比，动态语言通过直接解释脚本文件而执行的。这一机制使得动态语言在路径获取方面更为直观和易懂。然而，Go语言将源代码编译成独立的二进制可执行文件，这导致可执行文件与源代码间缺乏直接的联系。\n为了简化调试过程，Go 通过 go run 命令提供了一种类似动态语言直接执行源代码的便捷方式，实质上是将构建和运行步骤合二为一。这个过程中，会生成一个临时可执行文件，但这个文件不是存在当前工作目录中，这又为理解上带来额外的挑战。\n如果想找到这个文件，可通过 go run -work 保留文件，通过 os.Args[0] 确认文件路径。\nfunc main() { fmt.Println(os.Args[0]) } 输出：\n$ go run -work main.go WORK=/var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build1458488796 /var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build1458488796/b001/exe/main 可执行文件就是位于 $WORK/b001/exe/ 的 main 文件。\n若你习惯于动态语言中获取路径的做法，在 Go 中通过相对于可执行文件的路径来定位其他文件，使用 go run 调试的时候，就可能会引起一定的困惑。\n下面开始进入正题，详细 Go 中的文件路径的不同获取方式吧。\n相对于执行文件获取路径 # 之前提到了那么多在 Go 中获取可执行文件路径时可能导致的问题，我们就先从如何获取当前执行文件的路径开始吧。\n我将介绍实现这个目标的两种方式。\n命令行参数 os.Args[0] # 第一种方式是通过命令行参数 os.Args[0]。os.Args 是一个字符串切片，包含启动程序时传递给它的命令行参数。os.Args[0] 是这个切片的第一个元素，通常表示程序的执行文件路径。引言部分的演示示例，我就是通过这种方式获取执行文件的路径的。\n这个方式缺点是，依赖于可执行文件是被调用的方式，它可能是一个相对路径、一个绝对路径，或者仅仅是程序名。\n于是，为了保险起见，我们可通过 exec.LookPath 对 os.Args[0] 做一个处理。\nfmt.Println(exec.LookPath(os.Args[0])) 这个函数的作用是，输入参数 filename 中如果包含如 / 字符，直接返回 filename，否则会从 PATH 环境变量中寻找名为 filename 的可执行文件。这就解决了仅仅通过程序名调用无法获取文件路径的问题。\n我是在 MacOS 上测试的，这段逻辑是在 lp_unix.go 文件中，window 应该是不同的逻辑，windows 的文件路径分隔符和类 unix 不同，或者也有其他复杂逻辑。\n另外，它获取到的可能是相对路径也可能是绝对路径。如果希望得到绝对路径，要通过 filepath.Abs 处理下。\nexePath, _ := exec.LookPath(os.Args[0]) fmt.Println(filepath.Abs(exePath)) 但这种不是最优的方式，明显是绕的远了。 我提这个方法是为了顺便介绍下 exec.LookPath 和 filepath.Abs 这两个函数。\n使用 os.Executable # 获取当前 Go 程序的执行文件路径最优的解法是，使用 os.Executable 函数。这个方法会返回可执行文件的绝对路径。\nfmt.Println(os.Executable()) // 输出:\n$ go run main.go /var/folders/0b/v4r1lzyj0n566qgd8dt_km4c0000gn/T/go-build305466852/b001/exe/main \u0026lt;nil\u0026gt; 这个值在 go 启动时，运行时自动解析到内存的值，而调用 os.Executable 实际就是直接从这个变量中获取，没有额外的处理。\n它的性能相对于前面的通过几个函数组合实现的方式，肯定是吊打前者。\n但，这两种方式都没有解决一个问题：如果执行文件是符号链接，不会返回真正的可执行文件。\n符号链接 # 我们可通过使用 filepath.EvalSymlinks 来获取符号链接实际指向的路径。\nrealPath, _:= filepath.EvalSymlinks(exePath) fmt.Println(\u0026#34;Real path of executable:\u0026#34;, realPath) 兼容 go run 与 go build # 讲了那么多关于获取当前执行文件路径的方案，但如何解决由 go run 临时文件产生的问题呢？\n我的建议是，换个思路，不要把拘泥在相对于可执行文件定位其他文件路径这一个方向上。我在网上看到过通过判断是否是 go run 运行实现的适配方案。\n大概意思是，通过判断执行文件的运行目录或手动添加环境变量标识当前位于 go run 运行模式。如果处理 go run 模式下，我们再通过相对于源码文件位置定位其他文件。\n尝试实现下吧。\n// isGoRun 检查当前是否处于 go run 模式 func isGoRun() bool { // 检查环境变量（如果你选择设置一个特定的环境变量来标识） if _, ok := os.LookupEnv(\u0026#34;GO_RUN_MODE\u0026#34;); ok { return true } } 或者是\nfunc isGoRun() bool { // 或者通过分析 executable 路径的特征来判断 exePath, err := os.Executable() if err != nil { fmt.Println(\u0026#34;Error getting executable path:\u0026#34;, err) return false } // 示例中仅仅检查路径是否包含临时目录特征，实际情况可能需要更复杂的逻辑 return exePath[:5] == \u0026#34;/var/\u0026#34; { } 而在入口函数 main 中，通过 runtime.Caller(0) 获取源码文件路径。\nfunc EntryPath() string { if IsGoRun() { _, file, _, ok := runtime.Caller(0) if ok { return filepath.Dir(file) } } else { path, _ := os.Executable() return filepath.Dir(path) } return \u0026#34;./\u0026#34; } func main() { configPath := filepath.Join(EntryPath(), \u0026#34;config.json\u0026#34;) fmt.Println(\u0026#34;ConfigPath:\u0026#34;, configPath) } 除了那个获取源码文件位置的函数 runtime.Caller，这个代码并不复杂。runtime.Caller 函数用于获取当前函数的调用栈信息。\n它的函数签名，如下所示：\nfunc Caller(skip int) (pc uintptr, file string, line int, ok bool) 返回信息有调用者（main 函数）的程序计数器（PC）、文件名、代码行号、一个布尔值，布尔值表示获取信息是否成功。我们关心的是源码文件路径，runtime.Caller 返回的文件名可以用来确定当前执行代码的位置。\n看到这里，不知道是不是有人发出疑问，竟然通过能定位源码文件位置，为什么还要另外一种方式。这是源码文件的位置不会因执行文件的移动而变动。举例来说，如果 main.go 文件在 ~/Users/poloxue/ 下构建出 main 执行文件。我将其移动到其他目录，甚至是服务器上，它的路径依然是 /Users/poloxue/main.go。\n现在，即使在 go run 模式下，依然能正确定位其他文件的路径了。\n这种方式看起来挺不错的，但我不推荐。我的建议是，为项目定义清晰明确的规则来管理配置和资源文件的路径。\n定义明确的路径规则 # 常见的是用绝对路径规则指定配置和资源文件路径，如 Linux 或其他类 Unix 系统有一套 XDG 基准规则（XDG Base Directory Specification），有兴趣可了解下。\n或者是另一套更常见日常项目中的方案，通过环境变量或其他方式设置固定的项目根目录或工作目录，而其他文件路径皆相对于这个固定不变目录的位置。\n$RootDir/config.yaml $RootDir/logs/ $RootDir/resources/ $RootDir/static 实际上，这种方式更常见于平时的项目中。无论可执行文件被放在什么路径下，都不会对其他文件的路径位置产生影响。\n如果希望文件路径支持自定义，可在配置中提供路径配置项，或通过命令行选项的方式传递。\nlog_path = \u0026#34;/var/log/\u0026#34; 或\n$ go run main.go --config-path \u0026#34;./config.toml\u0026#34; 如果觉得每次 go run 都要带上环境变量麻烦，可提前设置环境变量\nexport ROOTDIR=`pwd` 我们也可以在 IDE 中设置项目级别的环境变量。\n亦或是提供默认值，如果 ROOTDIR 为空，默认项目根目录为 ./，即当前路径，\n# ROOTDIR=./ go run main.go $ go run main.go 如果是运行在 Docker 中，可通过 WORKDIR 指定工作目录，问题也变得简单很多。\n总结 # 在 Go 项目中正确处理文件路径是确保程序可移植性、稳定性和安全性的关键。与动态语言不同，Go编译成二进制可执行文件，使得直接关联源码和运行时文件变得复杂。\n本文介绍了多种获取文件路径的方法，包括 os.Args[0]、exec.LookPath、filepath.Abs和 os.Executable，并讨论了如何通过判断是否是 go run 运行来兼容 go run 和go build 的路径问题。\n最后，建议定义清晰的规则管理配置和资源文件路径，使用环境变量或配置项指定路径，避免依赖于可执行文件位置，以求提高 Go 项目的健壮性。\n感谢阅读，欢迎关注我的更多文章。\n","date":"2024-02-23","externalUrl":null,"permalink":"/posts/2024-02-23-get-filepath-in-golang/","section":"文章","summary":"嗨，大家好！我是波罗学。本文是系列文章 Go 技巧第十九篇，系列文章查看：Go 语言技巧。\n在使用 Go 开发项目时，估计有不少人遇到过无法正确处理文件路径的问题，特别是刚从如 PHP、python 这类动态语言转向 Go 的朋友，已经习惯了通过相对源码文件找到其他文件。这个问题能否合理解决，不仅关系到程序的可移植性，还直接影响到程序的稳定性和安全性。\n","title":"如何正确处理 Go 项目中关于文件路径的问题","type":"posts"},{"content":"嗨，大家好！我是波罗学。\n本文是系列文章 Go 技巧第十七篇，系列文章查看：Go 语言技巧。\n本文将介绍 Go 如何按行读取文件，基于此会逐步延伸到如何按块读取文件。\n引言 # 我们将要介绍的按行读取文件的方式其实是非常适合处理超大文件。\n按行读取文件相较于一次性载入，有着很多优势，如内存效率高、处理速度快、实时性高、可扩展性强和灵活度高等，特别是当遇到处理大文件时，这些优势会更加明显。\n稍微展开说下各个优势吧。\n内存效率高，因为是按行读取，处理完一行就会丢弃，内存占用将大大减少。\n处理速度快，主要体现在逐行处理时，因为无需等待全量数据，能更快开始，而且如果无顺序要求，还可并行计算以最大化利用计算资源，进一步提升处理速度。\n实时性高，因为按行读取，无需一次加载全量数据，自然有 实时性高 的特点，这对于处理实时流数据，如日志数据，非常有用。\n可扩展性强，按行读取这种方式，不仅仅适用于小文件，大文件同样使用，有了统一的处理方式，即使未来数据量膨胀，也易于扩展。\n灵活度高，因为是一行行的处理，如果想停止，随时可以。如果继续之前的流程，我们只要重新启动，从之前的位置继续处理即可。\n按行读取其实只是按块读取的一种特殊形式（分隔符是 \\n），自然地，上述的优势也同样适用于按块读取文件。\n本文的重点在于如何使用 GO 实现按行读取，基于的是标准库的 bufio.Reader 和 bufio.Scanner 。\n正式进入主题吧。\n准备一个文本文件 # 我们先准备一个文本文件 example.txt，内容如下：\nThis post covers the Golang Interface. Let’s dive into it. Duck Typing To understand Go’s interfaces, it’s crucial to grasp the Duck Typing concept. So, what’s Duck Typing? 基于 bufio.Reader # Go 中的按行读取文件，首先可通过 bufio 提供的 Reader 类型实现。\n使用 Reader.ReadLine # Reader 中有一个名为 ReadLine 的方法，顾名思义，它的作用就是按行读取文件的。\n演示代码：\nfile, err := os.Open(\u0026#34;example.txt\u0026#34;) if err != nil { panic(err) } defer func() { _ = file.Close() }() reader := bufio.NewReader(file) for { line, _, err := reader.ReadLine() // 按行读取文件 if err == io.EOF { // 用于判断文件是否读取到结尾 break } if err != nil { panic(err) } fmt.Printf(\u0026#34;%s\\n\u0026#34;, line) } 重点就是那句 line, _, err := reader.ReadLine()，返回值的第一值是读取的内容，第三个值是错误信息。\n执行与输出：\n$ go run main.go This post covers the Golang Interface. Let’s dive into it. Duck Typing To understand Go’s interfaces, it’s crucial to grasp the Duck Typing concept. So, what’s Duck Typing? 和我们预期的一样，输出了完整的文本信息。\n要提醒的是，ReadLine 读取的内容不包括行尾符（如 \u0026ldquo;\\r\\n\u0026rdquo; 或 \u0026ldquo;\\n\u0026rdquo;）。也就是说，当读取到一行数据时，要自行处理可能的行尾符差异，尤其是在处理来自不同操作系统的文本数据时。\n还有，ReadLine 省略的第二个参数，名为 isPrefix，它表示是否是前缀的意思，如果 isPrefix 为 true 表示返回的 line 被截断了，而截断原因很可能是行的内容大小大于缓冲区。我们可以在初始化时通过 bufio.NewReaderSize(rd io.Reader, size int) 调整默认缓冲区大小。\n不过，这并非最优的解法。\n使用 Reader.ReadString # 解决大行读取被截断的问题，还可用 bufio.Reader 的另外一个方法 ReadString 解决。\n它与 ReadLine 类似，不过在单个 buffer 不足以容纳单行内容时，它会多次读取，直到找到目标分割符，合并多次读取的内容。\n示例代码：\nreader := bufio.NewReader(file) for { line, err := reader.ReadString(\u0026#39;\\n\u0026#39;) if err == io.EOF { break } if err != nil { panic(err) } fmt.Printf(\u0026#34;%s\\n\u0026#34;, line) } 重点就是那句 reader.ReadString('\\n')，它的入参是分割符（delim），即 \u0026lsquo;\\n\u0026rsquo;，而返回值分别读取内容（line）和错误（err)。\n相较于 ReadLine，ReadString 显然是更加灵活，无大行读取被截断的问题，而且分割符也可自定义。但只支持单一字节的分割符自定义，还不够完美，如我们想按多个字符（如 .|, 等等）分割文本，或者按照大小分块读取，就没有那么方便了。\n我们继续引入另一个 Go 标准提供的按行读取文件的方案，即 bufio.Scanner。\n使用 bufio.Scanner # 为了由浅入深地介绍 bufio.Scanner 的使用，我们还是先从 bufio.Scanner 实现按行读取讲起吧。\n一个示例代码了解 bufio.Scanner 的基本使用。\n// 创建文件的扫描器，用于逐行读取文件 scanner := bufio.NewScanner(file) // 循环，直到文件结束 for scanner.Scan() { // 处理每行的内容：打印 fmt.Println(scanner.Text()) } // 最后，检查扫描过程中是否有错误发生 if err := scanner.Err(); err != nil { panic(err) } 这个例子中，我们基于打开的文件描述符 file，创建了一个 bufio.Scanner 变量 scanner，它通过 scanner.Scan() 逐行扫描文件和 scanner.Text() 从 buffer 中获取扫描内容，直到结束。\n毫无疑问，相对于 bufio.Reader，以上通过 bufio.Scanner 实现的代码简洁很多，而且，错误处理也是集中在 for 循环完成后统一进行。\n如何读取大行？ # bufio.Scanner 如何处理特别长的行呢？\n默认情况下，bufio.Scanner 初始缓冲区是 4KB，而最大 token 大小是 64KB，即无法处理超过 64KB 的行。\n来自源码中的定义，如下所示：\n// `MaxScanTokenSize` 可定义 buffer 中 token 的最大 size， // 除非用户通过 `Scanner.Buffer` 显式修改 // 缓冲区初始大小和 token 最大 size， // 实际的最大标记大小可能会更小，因为 // 缓冲区可能需要包含例如换行符之类的内容。 MaxScanTokenSize = 64 * 1024 // 缓冲区的初始大小 startBufSize = 4096 bufio.Scanner 中提供了 Scanner.Buffer() 方法可用于调整默认的缓冲区。\n示例代码：\nconst maxCapacity = 1024 * 1024 // 例如，1MB，可读取任何 1MB 的行。 buf := make([]byte, maxCapacity) // 初始缓冲大小 1MB，无需多次扩容 scanner.Buffer(buf, maxCapacity) 在 scanner 扫描前，加上这段代码，会重新设置缓冲区，将初始缓冲大小和最大容易都设置为 1MB，这样就可以处理异常长的大行（size \u0026lt;= 1MB）了，而且由于初始缓冲区大小就是最大容量，也无需多次扩容缓冲。\n缓冲区逻辑 # 为了更好理解上面的缓冲区配置，我简单介绍下 bufio.Scanner 是的 Scan 文件读取逻辑以及缓冲区是如何用的。\nbufio.Scanner 内部有一个 s.buf 缓冲区，当我们调用 scannder.Scan 方法时，它会尝试用 io.Reader（即示例中的 file 文件描述符）中读取一个缓存大小的内容。它的具体实现是在 bufio.Scanner 的 Scan 方法中。如果当缓冲区大小不足以容纳一个完整的 token，Scanner 会自动增加缓冲区的大小。\n接下来，让我们实现 bufio.Scanner 按单词读取。\n扩展思路 # 如果每次都读取这么大块的一整行，和一次载入没有什么区别，这明显已经失去了开头介绍的一行行读取的优势了。\n除了直接读取整行，是否还有什么更好的方法处理大行呢？\n我们可以尝试解放一些思路，是否还有其他方式定义一次读取内容呢？我们只要保证读取的内容有实际含义即可，如按一句话，一个单词或者固定的块大小的切割，而非是纠结于是不是一整行。\n分割规则定义 # 在正式介绍切割规则前，先说明下什么是完整 token。前面一直在说 token，如 MaxScanTokenSize 定义的就是 token 最大 size。\ntoken 定义其实就是对一次读取内容的定义，如一行文本，一个单词，或者一个固定大小的块。相对于特定分隔符，分割规则更加灵活，可以定义任意的分割方式。\n而 bufio.Scanner 是一个非常灵活的工具，它提供了自定义切割文本规则的函数 - Scanner.Split。\n// 参数 // data []byte: 未处理数据的初始子串，当前需要处理的输入数据。 // atEOF bool: 一个标志，如果为 true，则表示没有更多数据可处理。 // 返回值 // advance int: 需要在输入中前进多少以到达下一个标记的起始位置。 // token []byte: 要返回给用户的内容（如果有）。 // err error: 扫描过程中遇到的错误。 type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error) 它的返回是分别读取内容的长度、读取的内容和错误信息。\n默认情况下，Scanner 按行分割（ScanLines）。\nscanner.Split(bufio.ScanLines) // 默认配置，按行读取 我们可以通过自定义的 Split 函数改变这个默认行为，如按单词分割。\n示例代码：\nconst input = \u0026#34;This is a test. This is only a test.\u0026#34; scanner := bufio.NewScanner(strings.NewReader(input)) // 设置分割函数为按单词分割 scanner.Split(bufio.ScanWords) // 逐个读取单词 for scanner.Scan() { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, \u0026#34;reading input:\u0026#34;, err) } 输出：\nThis is a test. This is only a test. 现在，无论多大的文件，我们都可以通过巧妙定义切割方式来避免一次性读取的缺点了。\n我之前利用 whispwer 识别油管视频的字幕，有些视频的内容非常长，超长字幕，都在一行。现在我就可以通过如句号、问号、感叹号分割即可。现在，我要做的定义这样一个 ScanSentences 函数。\n示例代码：\nfunc ScanSentences(data []byte, atEOF bool) (advance int, token []byte, err error) { // 如果我们处于 EOF 并且有数据，则返回剩余的数据 if atEOF \u0026amp;\u0026amp; len(data) \u0026gt; 0 { return len(data), data, nil } // 定义一个查找任意句子结束符的函数 findSentenceEnd := func(data []byte) int { // 检查每个可能的句子结束符 endIndex := -1 for _, sep := range []byte{\u0026#39;.\u0026#39;, \u0026#39;?\u0026#39;, \u0026#39;!\u0026#39;} { if i := bytes.IndexByte(data, sep); i \u0026gt;= 0 { // 选择最小的 index 作为句子结尾 if i \u0026lt; endIndex || endIndex == -1 { endIndex = i } } } return endIndex } // 使用新的查找逻辑来查找句子结束位置 if i := findSentenceEnd(data); i \u0026gt;= 0 { // 返回找到的句子（包括句子结束符），以及下一个 token 的起始位置 return i + 1, data[:i+1], nil } return 0, nil, nil } 我们写个 main 函数测试下 ScanSentences 的正确性吧。\n示例代码：\nfunc main() { const input = \u0026#34;This is a test. This is only a test. Is this a test? \\n\u0026#34; + \u0026#34;Wow, what a brilliant test! Thanks for your help.\u0026#34; scanner := bufio.NewScanner(strings.NewReader(input)) scanner.Split(ScanSentences) for scanner.Scan() { text := scanner.Text() fmt.Printf(\u0026#34;%s\\n\u0026#34;, strings.TrimSpace(text)) } if err := scanner.Err(); err != nil { panic(err) } } 执行输出：\n$ go run main.go This is a test. This is only a test. Is this a test? Wow, what a brilliant test! Thanks for your help. 或者按照固定大小分批读取文件，SplitBatchSize 示例代码：\n// ScanBatchSize 返回一个 bufio.SplitFunc 函数，该函数按照固定的大小分割数据。 // 如果数据大小不足一个完整的批次，并且已经到达 EOF，则返回剩余的数据。 func ScanBatchSize(batchSize int) bufio.SplitFunc { return func(data []byte, atEOF bool) (advance int, token []byte, err error) { // 如果数据大小达到或超过批次大小，或者在 EOF 时有剩余数据 if len(data) \u0026gt;= batchSize || (atEOF \u0026amp;\u0026amp; len(data) \u0026gt; 0) { // 如果当前批次大小超过剩余数据大小，则只返回剩余数据 if len(data) \u0026lt; batchSize { return len(data), data[:], nil } // 否则，返回一个完整批次的大小和数据 return batchSize, data[:batchSize], nil } // 如果没有足够的数据并且没有到达 EOF，需要更多数据来形成一个完整的批次 if !atEOF { return 0, nil, nil } // 处理到达 EOF 但没有剩余数据的情况 return 0, nil, nil } } SplitBatchSize 是一个闭包，它的返回值是我们期待的 SplitFunc。我们可传递参数配置每次读取内容的大小。具体可自行测试，这里就演示了。\n不得不说 # 到这里，我还是想再提一点，每次从文件中读取内容大小是由传入系统调用 read() 函数时传入参数 buf 大小决定的，而不是由所谓按行还是按块确定的。按行按块是基于读取出来的二次处理的结果。\n之所以要提这点，因为我之前看到一些文章说，按块相比按行读取减少了读取的次数。\n结论 # 本文详细介绍了在 Go 中如何使用 bufio.Reader 和 bufio.Scanner 按行或按块读取文件，通过利用 GO 的标准库能力，我们有了更加灵活、高效处理大型文本文件的策略。\n最后，感谢阅读，希望本文对你有所帮助。\n","date":"2024-02-21","externalUrl":null,"permalink":"/posts/2024-02-21-readline-in-golang/","section":"文章","summary":"嗨，大家好！我是波罗学。\n本文是系列文章 Go 技巧第十七篇，系列文章查看：Go 语言技巧。\n本文将介绍 Go 如何按行读取文件，基于此会逐步延伸到如何按块读取文件。\n","title":"Go 如何按行读取（大）文件？尝试 bufio 包提供的几种方式","type":"posts"},{"content":"嗨！大家好，我是波罗学。本文是 Golang 三方库推荐第三篇，主要是闲扯，为后面两个工具做铺垫。系列查看：Golang 三方库。\n平时在开发项目时，常会需要改动数据库结构，比如说，有时候需要加个表，改个字段名，这是个挺常见的事。为了搞定这些改动，一般有两种做法：增量同步和差异同步。\n今天，简单聊聊这两种同步方式，看看它们各自有啥优点和缺点。我曾经在工作中即用过增量同步，也用过差异同步。\n增量同步 # 先说增量同步，这个方法挺直接的。就是每次你改了数据库，都会写一个迁移文件记录下来。想更新数据库的时候，就按这些文件的顺序，一步步来。\n我来通过一个实际场景介绍下这种同步方式。\n假设你有一个博客系统，现在需要为posts表增加一个views字段来记录每篇文章的浏览次数。使用增量同步，你会创建一个迁移文件，记录下这次变更。\n- 20210101_add_views_to_posts.sql ALTER TABLE posts ADD COLUMN views INT DEFAULT 0; 如果之后决定添加一个comments_count字段来记录评论数，你会创建另一个迁移文件：\n-- 20210202_add_comments_count_to_posts.sql ALTER TABLE posts ADD COLUMN comments_count INT DEFAULT 0; 使用增量同步工具，就会按照文件的顺序来更新数据库结构。我下篇文章介绍的一个 Go 实现的表结构同步同居 Goose 就是基于这种方式。\n这种方式的好处在于，我们能很清楚地知道每次的改动，需要回头看的时候，也容易找。而且，这些迁移文件可以放到版本控制里，团队里的成员都能看到，一起协作。\n不太好的地方是，时间一长，这些文件可能成堆积如山，管理起来就头疼了。特别是新起一个环境，得从头跑一遍所有的迁移，有时候还会中途卡壳。我自己就遇到过，为了让一个新环境跑起来，得一个一个检查哪里出了问题，中间还得修十几个迁移文件，那时候真是既烦躁又无奈。\n针对这个问题，我原本是寄希望于找到一款支持类似 git squash 能力的工具，能把一段时间的 SQL 改动合并成一个文件，按发布版本管理迁移文件。这样至少文件少点，依赖问题也能减轻些。不过，貌似大多数增量迁移工具没有这个能力。当然，即使支持也支持缓解，并没有根治。\n如果走手动合并的路线，版本控制和环境隔离就成了大问题，需要精心规划，多人审查来控制发布。\n差异同步 # 差异同步呢，就比较关注起点和终点。\n它看看你的数据库从现在到你想要的样子之间有啥不同，然后直接生成一个脚本，把数据库一次性改到你想要的样子。\n还是以上面的增量同步中介绍的场景为例。假设你的posts表初始状态没有views和comments_count字段，目标状态需要这两个字段，使用差异同步工具，它会生成如下的SQL脚本：\nALTER TABLE posts ADD COLUMN views INT DEFAULT 0, ADD COLUMN comments_count INT DEFAULT 0; 这个脚本包含了将数据库从当前状态更新到目标状态所需的所有变更。\n优点是管理起来轻松多了，不用像增量那样一大堆文件。对于那种经常改来改去的项目，用这个可以省不少事。\n缺点是这种方式有时候没那么精确。自动生成的脚本偶尔可能也会有疏漏，需要人工检查一下。\n虽然说，差异同步的方式有这样的缺点，但我们可以将生成差异文件交给专业的人和自动化流程检查，它也更容易标准化到 CI/CD 流程中，容易 linter 检查。\n我也会介绍一个 Go 实现的基于这个方式的工具来完成这种操作。\n实际操作建议 # 具体挑哪个方式，得看你的项目需要。如果你特别在意每一步的变化，喜欢细致控制，甚至是具体到个人，增量同步可能更合适。但如果你更看重效率，不想被一堆文件搞得头大，甚至希望能自动化流程。那就试试差异同步。\n不管用哪种，我都有几个小建议：\n非生产环境测试 # 在对数据库结构进行修改前，一定要在非生产环境进行彻底测试。这不仅是为避免直接在生产环境中引入潜在问题，更是为了确保新的变更不会破坏现有的数据或功能。特别是当你尝试合并增量迁移文件以简化管理时，测试成为了一个不可或缺的步骤。\n版本控制记录 # 将数据库变更记录纳入版本控制系统是管理数据库迁移的另一个关键。这样做不仅能够保持团队成员之间的同步，还能提供一个详细的变更历史，方便回溯和审计。对于使用差异同步方式的项目，虽然变更可能是由当前与目标状态之间的差异自动生成的，但将迁移脚本的变更历史记录都保存下来了，可以追踪每次部署的具体内容。如果在未来遇到问题，这些记录也可以帮助快速定位问题的起源，从而简化故障排除过程。\n尽量自动化 # 自动化迁移过程不仅可以提高效率，还能减少因手动操作导致的错误。通过使用CI/CD自动执行迁移，可以确保每次部署都经过相同的步骤，减少了人为失误的机会。此外，自动化测试可以与迁移过程结合起来，以自动验证每次变更的影响，确保迁移不会对现有系统造成意外的破坏。。\n总结 # 管理数据库结构的变更是挺需要技巧的一件事。了解增量同步和差异同步的各自优劣，能让我们根据实际情况，选择更合适的方法。\n对于我个人来说，增量同步的使用经历的确是不好受。一般情况下，我会优选差异同步。\n","date":"2024-02-20","externalUrl":null,"permalink":"/posts/2024-02-26-database-structure-migration/","section":"文章","summary":"嗨！大家好，我是波罗学。本文是 Golang 三方库推荐第三篇，主要是闲扯，为后面两个工具做铺垫。系列查看：Golang 三方库。\n平时在开发项目时，常会需要改动数据库结构，比如说，有时候需要加个表，改个字段名，这是个挺常见的事。为了搞定这些改动，一般有两种做法：增量同步和差异同步。\n","title":"数据库结构变更同步：增量 VS 差异","type":"posts"},{"content":"嗨，大家好！我是波罗学。本文是系列文章 Go 技巧第十八篇，系列文章查看：Go 语言技巧。\n目录遍历是一个很常见的操作，它的使用场景有如文件目录查看（最典型的应用如 ls 命令）、文件系统清理、日志分析、项目构建等。\n本文将尝试逐步介绍在 Go 中几种遍历目录文件的方法，从传统的 ioutil.ReadDir 函数开始，逐渐深入。\n文中也会提供示例代码、提供一些性能剖析，以便于大家更好地理解。\nioutil.ReadDir # 首先，Go 中目录文件遍历的第一种方式是 ioutil.ReadDir 函数。\n在 Go 1.16 版本前，ioutil.ReadDir 就是遍历目录的标准方法，它的返回结构是目录中文件的 FileInfo 列表，简单直接。\n示例代码：\nfunc main() { files, err := ioutil.ReadDir(\u0026#34;.\u0026#34;) if err != nil { log.Fatal(err) } for _, f := range files { fmt.Println(f.Name()) } } 但它的缺点也非常明显，性能不高。导致它的主要原因有如下几点：\n完全加载\n这就导致了 ioutil.ReadDir 在返回结果前，会将目录下所有文件的信息完全加载到内存中。对于包含大量文件的目录，它就需要在内存中存储大量的 FileInfo 对象，毫无疑问，这会增加内存使用。\nFileInfo 开销\n由于是完全加载，每个 FileInfo 对象都包含了文件的详细信息，如文件名、大小、修改时间等都会在返回之前都已经加载完成。但获取这些信息需进行系统调用。而每个文件都要做这样的调用，当文件数量很多时，这些系统调用的累积开销可以变得不容忽视了。\n无法分批处理\n由于 ioutil.ReadDir 是一次性返回所有文件信息，没有提供分批处理的能力。无论目录中有多少文件，都要等待所有文件信息读取完成，这在处理目录中包含大量文件的场景中，也就无法提前并行处理，效率是可想而知的。\n这一点其实和我们前面的一篇文章，介绍的 GO 中按行（或者说按块）读取文件的逻辑是类似的，一次加载全部内容，有潜在的性能问题。\n由于 ioutil.ReadDir 有这么多的缺点，所以它在 Go 1.16 及更高版本已经被弃用了。\n那现在我们该用什么方法呢？\nos.ReadDir # 从 Go 1.16 版本起，标准库针对目录遍历查看提供了新的函数 os.ReadDir，以用来简化和提高遍历目录文件的效率。\n函数签名如下：\nfunc ReadDir(name string) ([]DirEntry, error) os.ReadDir 函数返回一个按文件名排序的 DirEntry 类型切片。如果在读取目录项时遇到错误，它也会尽量返回已读取内容。这种设计同时兼顾了效率和错误处理的需要。\n示例代码：\nfunc main() { files, err := os.ReadDir(\u0026#34;.\u0026#34;) if err != nil { log.Fatal(err) } for _, file := range files { fmt.Println(file.Name()) } } os.ReadDir 相比于旧方法 ioutil.ReadDir 的有什么优势？为什么丢弃 ioutil.ReadDir 而引入这个新的 os.ReadDir。\n如果对比两者源码，会发现差异主要在返回的类型上。os.ReadDir 返回的 []DirEntry 而非 []FileInfo。它还具有性能优势。\n为什么？\n因为 DirEntry 允许按需获取文件详情，即懒加载，而非是遍历目录时立即加载所有文件属性。很多场景下，我们并不需要\n我在 MacOS 系统下测试的 DirEntry 接口的实际变量类型为 os.unixDirent。\n它的源码如下：\nfunc (d *unixDirent) Name() string { return d.name } func (d *unixDirent) IsDir() bool { return d.typ.IsDir() } func (d *unixDirent) Type() FileMode { return d.typ } func (d *unixDirent) Info() (FileInfo, error) { if d.info != nil { return d.info, nil } return lstat(d.parent + \u0026#34;/\u0026#34; + d.name) } 我们只有在调用 Info 方法时，才会真正通过 lstat 发起系统调用。\n如果你有将旧代码迁移到 DirEntry 的需求， Go 1.17 还引入了 fs.FileInfoToDirEntry 函数，允许我们将 FileInfo 对象转换为 DirEntry 对象。\ninfo, _ := os.Stat(\u0026#34;somefile\u0026#34;) dirEntry := fs.FileInfoToDirEntry(info) 看到这，对于认真思考的朋友，或许已经发现我们还有一个问题没解决，即 os.ReadDir 不是也不支持分批处理的能力吗？\n继续往下看吧，我将介绍一个更底层的方法。\nos.File 的 ReadDir 方法 # 我们知道 os.Open 是用于打开文件的，但其实它也可用于打开目录。如果 os.Open 打开的是目录，我们在它返回的 os.File 上调用 ReadDir 以查看目录内容。\n示例代码：\nfunc main() { dir, err := os.Open(\u0026#34;.\u0026#34;) if err != nil { log.Fatal(err) } defer dir.Close() files, err := dir.ReadDir(-1) if err != nil { log.Fatal(err) } for _, file := range files { fmt.Println(file.Name()) } } 如上的代码其实类似于 os.ReadDir 内容的实现代码。\nos.ReadDir 源码如下：\nfunc ReadDir(name string) ([]DirEntry, error) { f, err := Open(name) if err != nil { return nil, err } defer f.Close() dirs, err := f.ReadDir(-1) sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() \u0026lt; dirs[j].Name() }) return dirs, err } 这种方法更底层，提供了更多的灵活性。我们就可以用它分批读取目标。\n如何实现呢？\n核心就是那句的 dir.Readdir(-1)，它的入参指定了每次读取文件的数量，而 -1 表示读取目录的所有内容。我们只要将 -1 改为分批读取的数量即可，多次循环即可。\n示例代码：\nfunc main() { dir, err := os.Open(\u0026#34;.\u0026#34;) if err != nil { log.Fatal(err) } defer dir.Close() for { files, err := dir.ReadDir(10) // 每批读取10个条目 if err == io.EOF { break // 遍历完成 } if err != nil { log.Fatal(err) // 处理其他错误 } for _, file := range files { fmt.Println(file.Name()) } } } 这段代码演示了如何使用 File.Readdir 分批处理目录中的文件。通过这种方式，可以更有效地管理内存使用。\n补充一点 # 在写这篇文章时，我发现 os.File 有两个查看目录的方法，分别是 Readdir 和 ReadDir。功能的区别的新的 ReadDir 返回的是 []DirEntry，而 Readdir 返回的是 []FileInfo。\n换句话说，ReadDir 本质上是 Readdir 的升级版。\n它们的函数签名，如下所示：\nfunc (f *File) Readdir(n int) ([]FileInfo, error) func (f *File) ReadDir(n int) ([]DirEntry, error) 这是因为不支持可选参数和重载但要解决兼容问题采取的措施吗？真的是蚌埠住了。\n目录的递归遍历 # 现在，还差最后一个内容没有介绍，那就是递归目录遍历。\n针对目录的递归遍历，Go 中提供了一个专门的函数，filepath.Walk。它可以遍历指定目录下的所有子目录。\n示例代码：\nfunc main() { err := filepath.Walk(\u0026#34;.\u0026#34;, func(path string, info os.FileInfo, err error) error { if err != nil { return err } fmt.Println(path) return nil }) if err != nil { fmt.Printf(\u0026#34;error walking the path %v: %v\\n\u0026#34;, \u0026#34;.\u0026#34;, err) } } 我们通过遍历的回调函数中在处理每个文件。它简化了目录的递归遍历，但对于大型或深层次的目录结构，同样存在着提前加载 FileInfo 的问题。\n针对这个问题，在 Go1.16 版本也引入了基于 DirEntry 版的 filepath.WalkDir 函数。\nfilepath.WalkDir 的函数签名如下：\nfunc WalkDir(root string, fn fs.WalkDirFunc) error fs.WalkDirFunc 的定义如下：\ntype WalkDirFunc func(path string, d DirEntry, err error) error 新函数的遍历回调参数是 DirEntry，而非 FileInfo。现在，filepath.WalkDir 也有了延迟加载 FileInfo 的能力了。\n现在，我们再来看下这张图。\n总结 # 在本文中，我们系统介绍了 Go中多种遍历目录文件的方法。从传统的 ioutil.ReadDir，到 Go 1.16 引入的 os.ReadDir，os.File 的 ReadDir 方法。每种方法适用于不同的场景，如何选择要取决于你的需求、Go 版本、性能。如果你需要递归遍历，也可以使用基于 DirEntry 的 filepath.WalkDir 实现，提高遍历的性能。\n最后，感谢阅读，请持续关注我的更多文章。\n博客地址：Go 中如何遍历目录？探索几种方法\n","date":"2024-02-20","externalUrl":null,"permalink":"/posts/2024-02-22-list-directory-in-golang/","section":"文章","summary":"嗨，大家好！我是波罗学。本文是系列文章 Go 技巧第十八篇，系列文章查看：Go 语言技巧。\n目录遍历是一个很常见的操作，它的使用场景有如文件目录查看（最典型的应用如 ls 命令）、文件系统清理、日志分析、项目构建等。\n","title":"Go 中如何高效遍历目录？探索几种方法","type":"posts"},{"content":"嗨！大家好，我是波罗学。本文是 Golang 三方库推荐第二篇，系列查看：Golang 三方库。\n今天介绍一个 TUI 库 - Bubble Tea，一个小巧但强大的文本用户界面（TUI）框架，基于 Go 语言开发。\n什么是 TUI？ # 什么是 TUI？为了防止可能有人不知道，简单解释一下。\nTUI，全称为文本用户界面（Text-based User Interface），是一种用户界面类型。对之对比的就是 GUI（Graphical User Interface）。我开始还以为 TUI 中的 T 的全称是 terminal。\nTUI 是通过纯文本和基本字符在终端或控制台窗口中展示信息和交互选项。与 GUI 不同，TUI 不依赖图形元素如窗口、图标或鼠标指针，而是使用文本字符来构建用户界面的布局和元素。\n如同我们开发 GUI 应用需要 GUI 框架的帮助，开发 TUI 应用也需要 TUI 框架的帮助，而 Bubble Tea 就是这样一款 TUI 框架。\n使用 Bubble Tea # Bubble Tea 受到 The Elm Architecture 的启发，基于这种有趣、功能性强且保持状态的架构来构建终端应用程序。无论是简单的行内应用程序、全窗口应用程序，还是两者的混合，Bubble Tea 都可使用。\n要学会基于 Bubble Tea 开发终端应用，核心要理解它的三个基本部分组成：模型（Model）、更新函数（Update）和视图函数（View）。而 Update 和 View 都是属于 Model 的方法。\n简单解释下它们的作用，如下:\n模型（Model）：描述应用程序状态的数据结构。 更新函数（Update）：处理输入事件（如按键、计时器等），并更新模型。 视图函数（View）：根据模型的当前状态渲染用户界面。 不明白？我们来通过一个例子解释下。\n一个简单的例子：TUI 的计数器应用。 # 这是一个计数器的案例，我们通过输入 \u0026lsquo;+\u0026rsquo; 号增加计数值，减号减少计数值，输入 \u0026lsquo;q\u0026rsquo; 退出应用。\n计数器的效果图，如下所示：\n如何基于 Bubble Tea 实现这个小应用呢？我们只要实现一个简单的 Model 即可。\n如下是它最核心的 model 代码。\ntype model int // 初始化模型，设置计数器的初始值 func initialModel() model { return 0 } func (m model) Init() tea.Cmd { return nil } // 更新函数，根据消息更新模型 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case \u0026#34;q\u0026#34;: return m, tea.Quit // 按下 \u0026#39;q\u0026#39; 退出程序 case \u0026#34;+\u0026#34;: return m + 1, nil // 按下 \u0026#39;+\u0026#39; 增加计数 case \u0026#34;-\u0026#34;: return m - 1, nil // 按下 \u0026#39;-\u0026#39; 减少计数 } } return m, nil } // 视图函数，渲染界面 func (m model) View() string { return fmt.Sprintf(\u0026#34;Count: %d\\nPress + to increment, - to decrement, q to quit.\\n\u0026#34;, m) } 观察以上代码，model 是一个基于 int 创建的新类型，用于保存应用的状态信息，即计数值。initialModel 创建一个初始值为 0 的 model。model 还提供了两个核心方法，分别是 Update 和 View。\n如上段所述，Update 是用于接收用户消息，更新 model 状态。View 函数用于更新界面内容，展示给用户，它的更新通过 Printf 打印即可。\n现在，只需将 Model 实例交给 Bubble Tea 框架运行皆可。\nfunc main() { p := tea.NewProgram(initialModel()) if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, \u0026#34;Error running program: %v\u0026#34;, err) os.Exit(1) } } 这个应用到此就开发完成。\n核心流程 # 通过 Counter 计数器这个简单案例，重新理解下 Bubble Tea 中 The Elm Architecture 数据流向。\n用户与界面交互，触发消息。 消息被发送到 Update 函数，Update 函数根据消息更新 Model。 更新后的 Model 被传递给 View 函数，View 函数生成新的界面显示给用户。 现在，理解起来是不是非常简单了。\n一个小游戏：贪吃蛇 # 基于 Bubble Tea 框架，开发了一个非常简单粗糙的贪吃蛇游戏，不会增加蛇的长度，效果如下：\nS 表示 snake，F 表示 Food，通过 w -\u0026gt; up, s -\u0026gt; donw, a -\u0026gt; left, d -\u0026gt; right 控制方向。\n代码地址：贪吃蛇游戏。有兴趣可自行扩展。\n更多示例 # 我们通过一个简单的计数案例解释了 Bubble Tea 的使用。如果想了解它的更多能力，可参考 Bubble Tea 源码中的 examples 了解它更多的能力。\n来自仓库中的一些案例效果图：\n更多查看：bubble tea\u0026rsquo;s examples。\n此外，仓库中还提供了教程，帮助我们理解如何使用这个框架构建终端应用程序。\n其他 TUI 框架 # 相对而言，bubble tea 提供构建终端应用的基本框架，但如果你希望开发非常复杂应用，它确实还是有一些拙荆见肘，如常用的窗口组件或基本的布局能力，它是没有提供的。\n这里再推荐 Go 中其他一些可供选择的 TUI 框架。\n首先是 tview，一个基于 tcell 的富文本终端应用程序框架，提供了构建复杂交互 TUI 所需的组件和布局管理器。它支持颜色、表格、表单、列表等多种控件，非常适合构建复杂的 TUI 应用。\n其次是 termui:，一个基于 Go 的 TUI 库，灵感来源于 blessed-contrib 和 tui-rs。它提供了创建数据可视化和仪表板的简单方法，支持条形图、线图、饼图等多种图表类型，适用于构建需要数据展示的 TUI 应用。不过这个库已经很久没有更新了，如果你熟悉 rust，推荐使用 tui-rs。\n总结 # 本文主要推荐一个 Golang 的 TUI 开发库 - Bubble Tea，如果你对这个话题感兴趣，或者有相关需求，可考虑一试。\n博文地址：推荐一个可用于快速创建 TUI 应用的框架 - Bubble Tea\n","date":"2024-02-19","externalUrl":null,"permalink":"/posts/2024-02-19-tui-library-bubble-tea-in-golang/","section":"文章","summary":"嗨！大家好，我是波罗学。本文是 Golang 三方库推荐第二篇，系列查看：Golang 三方库。\n今天介绍一个 TUI 库 - Bubble Tea，一个小巧但强大的文本用户界面（TUI）框架，基于 Go 语言开发。\n","title":"推荐一个可用于快速创建 TUI 应用的框架 - Bubble Tea","type":"posts"},{"content":"嗨，大家好！我是波罗学。本文是系列文章 Go 技巧第十六篇，系列文章查看：Go 语言技巧。\nGo 中有一个特别的 init() 函数，它主要用于包的初始化。init() 函数在包被引入后会被自动执行。如果在 main 包中，它也会在 main() 函数之前执行。\n本文将以此为主题，介绍 Go 中 init() 函数的使用和常见使用场景。还有，我在工作中更多看到的是 init() 函数的滥用。\ninit() 函数的执行时机 # 首先，init() 的执行时机处于包级别变量声明和 main() 函数执行之间。\n这意味着在包中声明的全局变量，如果附带初始化表达式，这些表达式将在任何 init() 函数执行之前进行初始化。\n我们通过一个示例演示，代码如下：\nvar Age = GetAge() func GetAge() int { return 18 } func init() { fmt.Printf(\u0026#34;You\u0026#39;re %d years old.\\n\u0026#34;, Age) Age = 3 } func main() { fmt.Printf(\u0026#34;You\u0026#39;re %d years old.\\n\u0026#34;, Age) } 输出：\nYou\u0026#39;re 18 years old You\u0026#39;re 3 years old 从输出可知，GetAge() 函数作为 Age 的初始化函数，于 init() 函数前执行，赋值 Age 为 3。而 init() 函数于其后执行，赋值 Age 为 3。main() 函数则在最后执行，输出最终的 Age 值。\n这个顺序是符合我们预期的。\n与被引入包的 init() 函数 # 如果一个包导入了其他包，被导入包的初始化 init() 则会先于导入它的包的变量初始化和 init 函数前执行。\n举例来说明吧！\n假设，我们有一个 main 包，它导入了 sub 包，并且同样有一个 init() 函数：\n// main.go package main import ( \u0026#34;fmt\u0026#34; _ \u0026#34;demo/sub\u0026#34; ) var age = GetAge() func GetAge() int { fmt.Println(\u0026#34;main initialize variables.\u0026#34;) return 18 } func init() { fmt.Println(\u0026#34;main package init\u0026#34;) } func main() { fmt.Println(\u0026#34;main function\u0026#34;) } 而 sub 包中包含定义的 init() 函数\n// sub/sub.go package sub import \u0026#34;fmt\u0026#34; var age = GetAge() func GetAge() int { fmt.Println(\u0026#34;sub initialize variables.\u0026#34;) return 18 } func init() { fmt.Println(\u0026#34;sub package init\u0026#34;) } // 其他可能的函数和声明 当你运行 main.go 时，输出将会按照以下顺序出现：\nsub initialize variables. sub package init main initialize variables. main package init main function 这个示例清晰地展示了包的初始化顺序：首先是被导入包（sub）的 init() 函数，然后是导入它的包（main）的 init() 函数，最后是 main 函数。\n这也确保了依赖包在使用前已经被正确初始化。\n特别说明：\ninit() 区别于其他函数，不需要我们显式调用，它会自动被 Go runtime 调用。而且，每个包中的 init() 只会被执行一次。\n一个包其实可有多个 init()，无论是在分部在包中的同一个文件中还是多个文件中。如果分布在多个文件中，执行顺序通常是按照文件名的字典顺序。\n为说明这个问题，我们首先修改 sub.go 文件，内容如下：\n// sub/sub.go package sub import \u0026#34;fmt\u0026#34; var age = GetAge() func GetAge() int { fmt.Println(\u0026#34;sub initialize variables.\u0026#34;) return 18 } func init() { fmt.Println(\u0026#34;sub init 1\u0026#34;) } func init() { fmt.Println(\u0026#34;sub init 2\u0026#34;) } 新增一个 sub1.go 文件，如下所示：\n// sub/sub1.go package sub import \u0026#34;fmt\u0026#34; var age = GetAge1() func GetAge1() int { fmt.Println(\u0026#34;sub1 initialize variables.\u0026#34;) return 18 } func init() { fmt.Println(\u0026#34;sub1 init\u0026#34;) } 输出：\nsub initialize variables. sub init 1 sub init 2 sub1 initialize variables. sub1 init main initialize variables. main package init main function 结果符合预期。\ninit() 的使用场景 # init() 函数通常用于进行一些必要的设置或初始化操作，例如初始化包级别的变量与命令行参数、配置加载、环境检查、甚至注册插件等。\n项目开发中，组件依赖管理通常比较令人头疼。但一些简单的依赖关系，即使没有如 wire 这样依赖注入工具的加持，通过 init 也可管理。\n命令行参数 # 对于开发一个简单的命令行应用，init() 和标准库 flag 包结合，可快速完成命令命令行参数的初始化。\npackage main import ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; ) var name string var help bool func init() { flag.StringVar(\u0026amp;name, \u0026#34;name\u0026#34;, \u0026#34;World\u0026#34;, \u0026#34;a name to say hello to\u0026#34;) flag.StringVar(\u0026amp;help, \u0026#34;name\u0026#34;, \u0026#34;World\u0026#34;, \u0026#34;display help information\u0026#34;) flag.Parse() } func main() { if help { fmt.Fprintf(os.Stderr, \u0026#34;Usage of %s:\\n\u0026#34;, os.Args[0]) flag.PrintDefaults() os.Exit(1) } fmt.Printf(\u0026#34;Hello, %s!\\n\u0026#34;, name) } 以上示例中，init() 函数解析了命令行参数并初始化变量 name 和 help 变量。\n配置加载 # init 函数的领哇一个常见场景是配置加载。配置通常是程序启动时要尽早执行的操作。\n例如，你有一个 web 服务，要在启动服务器前加载数据库配置、API 密钥或其他服务配置。\nvar config AppConfig func init() { configFile, err := os.Open(\u0026#34;config.json\u0026#34;) if err != nil { log.Fatal(err) } defer configFile.Close() jsonParser := json.NewDecoder(configFile) jsonParser.Decode(\u0026amp;config) } 如果配置加载都出现问题，很大程度说明服务配置不正常，要立刻退出服务。我们可使用 log.Fatal(err) （更优雅）或 panic(err) 退出服务。\n环境检查 # init() 还可以用于检查和验证程序运行所需的环境。如，我们要确保必要的环境变量已设置，或者必要的外部服务可用。\n如我们的必须依赖一个需要认证的外部服务，示例代码：\nfunc init() { if os.Getenv(\u0026#34;XXX_API_KEY\u0026#34;) == \u0026#34;\u0026#34; { log.Fatal(\u0026#34;XXX_API_KEY environment variable not set\u0026#34;) } apiKey := os.Getenv(\u0026#34;XXX_API_KEY\u0026#34;) // instantiating Component // ... } 通过，如果要实例化的组件不需要赖加载，创建和配置验证同时 init() 中完成即可。\n注册插件或服务 # 如果你的程序用的是插件架构，我们可以在程序启动时注册这些插件。init() 正可以用来自动注册这些插件。\n示例代码：\nfunc init() { plugin.Register(\u0026#34;myPlugin\u0026#34;, NewMyPlugin) } Go 的数据库驱动管理可作为这种场景的典型案例。\nGo 的 database 操作通常依赖 database/sql 包，它提供了一种通用接口与 SQL 或类 SQL 数据库交互。而具体的驱动实现（如 MySQL、PostgreSQL、SQLite 等）通常是通过实现 database/sql 包定义接口来提供支持。\n这种架构下，init() 被用于驱动的自动注册。\n例如，如下这个 MySQL 驱动的实现：\npackage mysql import ( \u0026#34;database/sql\u0026#34; ) func init() { sql.Register(\u0026#34;mysql\u0026#34;, \u0026amp;MySQLDriver{}) } type MySQLDriver struct { // 驱动的实现 } 我们只要导入这个 database driver 包，它的 init() 就会被调用，将驱动注册到 database/sql 包中。\n我们使用的时候，通过 database/sql 接口即可使用该 MySQL 驱动，而不需关心它的实现细节。\nimport ( \u0026#34;database/sql\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; // 导入 MySQL 驱动 ) func main() { db, err := sql.Open(\u0026#34;mysql\u0026#34;, \u0026#34;user:password@/dbname\u0026#34;) // ... } 通过这种方式，Go 的数据库驱动代码更加模块化和灵活性。使用方只需关心与 database/sql 交互即可，而不必关心驱动的实现细节。\n实际的场景案例，我觉得肯定不止这么多。对于任何需要提前初始化和验证的场景，可适当考虑是否可通过使用 init() 来简化代码。\n注意点 # 讲了那么多 init() 的使用，但我在平时发现，更多的时候 init() 函数是在被滥用。\n我这里不得不提一些注意点。\n启动耗时 # 首先，由于 init() 函数在程序启动时自动执行，这就导致它会增加程序启动时间，特别是一些组件初始化耗时较长。\n非必要场景，懒加载依然是不错的选择。\n什么是必要场景呢？简单来说，如果这个操作失败了，这个程序就没有继续启动的必要了。\n依赖关系 # 还有，过多或过于复杂的 init() 函数可能会导致程序难以理解维护，依赖关系混乱。\n这点在单体项目中体现的特别明显，所有人维护一个项目，所以依赖都加载到 init() 中。\n如何解决呢？\n如前面所有，一方面要仅在必要场景时使用 init() 函数初始化一些操作。\n另外，有条件的话，建议尽量保持服务简单，如果依赖过多，如出现要一个服务连接多个相同组件（数据库、Redis），就是时候考虑优化系统设计了，可考虑将部分业务抽离为独立服务。\n总结 # 本文介绍了到 init() 函数在 Go 中的特殊之处和使用方式。它提供了一种不同于其他语言的机制来初始化包，但也需谨慎使用以避免不必要的复杂性。\n最后，希望这篇文章能帮助你更好地理解和使用 Go 的 init() 函数。\n感谢阅读。\n博客地址：Go 中的 init 如何用？它的常见应用场景有哪些呢？\n","date":"2024-02-18","externalUrl":null,"permalink":"/posts/2024-02-18-init-function-in-golang/","section":"文章","summary":"嗨，大家好！我是波罗学。本文是系列文章 Go 技巧第十六篇，系列文章查看：Go 语言技巧。\nGo 中有一个特别的 init() 函数，它主要用于包的初始化。init() 函数在包被引入后会被自动执行。如果在 main 包中，它也会在 main() 函数之前执行。\n","title":"Go 中的 init 如何用？它的常见应用场景有哪些呢？","type":"posts"},{"content":"嗨，大家好！我是波罗学。\n本文是系列文章 Go 技巧第十五篇，系列文章查看：Go 语言技巧。\n我们先看这样一个问题：“Go 语言中，将 byte 转换为 int 时是否涉及字节序（endianness）？我可以直接使用 int(byte_var) 进行转换吗？”\n这个问题非常简单，直接回答不涉及字节序，可以直接转换。但为什么呢？如果要彻底搞明白这个问题，还是要了解下字节序这个概念。\n接下来，让我带你深入地了解这个问题，以及如何在 Go 中如何处理字节序。\n字节序 # 我们先解释一下什么是字节序？\n字节序，或称为字节顺序，即数据在内存中存储的字节顺序。字节序主要有两种：大端和小端。\n什么是大端模式？\n大端模式指的是高位字节（0x12）存储在低地址位（0）。\n什么是小端模式？\n小端模式指的是低位字节（0x78）存储在低地址位（0）。\n将 byte 和 int 相互转换 # 首先，int 如何转为 byte？\n在 Go 中，byte 是 int8 的别名，占用一个字节。由于它只有一个字节，自然不存在字节序的说法。\nvar byteVar byte = 0x78 intVar := int(byteVar) 我们将一个 byte 变量转换为 int 类型，byte 只占用一个字节，所以没有字节序的问题。当然，一定要说有字节序，也可以。毕竟，将 byte 转为 int 时，其实是将 byte 数值存在 int 低位，而不是高位。\n那么，将 int 转换为 byte 呢？当从 int 类型转换为 byte 时，字节序变得重要了。\n从 int 转为 byte 时，将会截断 int 数据，将最低位的数值作为 byte 的值。\n那么，如果我们想判断自己电脑上的字节序，只要将 int 转为 byte，即可判断。\n示例代码，如下所示：\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { s := int32(0x12345678) b := byte(s) fmt.Printf(\u0026#34;0x%x\\n\u0026#34;, b) } 输出：\n0x78 我的电脑上的输出结果为 0x78，它是低位的值，即低位存放于低地址。这表明我的机器是小端模式。\n网络传输与 Go 的 encoding/binary 包 # 在网络传输中，字节序至关重要。通常，网络协议要求使用大端字节序。当在不同字节序的系统之间通信时，正确处理字节序至关重要。\nGo 的 encoding/binary 包提供了处理字节序的便利工具。它定义了一个 ByteOrder 接口，包括各种转换函数。\n它的使用非常简单，代码如下：\npackage main import ( \u0026#34;encoding/binary\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { bytes := []byte{0x78, 0x56, 0x34, 0x12} fmt.Printf( \u0026#34;LittleEndian: 0x%x\\n\u0026#34;, binary.LittleEndian.Uint32(bytes), ) fmt.Printf( \u0026#34;BigEndian: 0x%x\\n\u0026#34;, binary.BigEndian.Uint32(bytes), ) } 我们使用 binary.LittleEndian 和 binary.BigEndian 完成小端和大端字节向 uint32 的转换。\n输出:\nLittleEndian: 0x12345678 BigEndian: 0x78563412 小端模式下，结果是 0x12345678，在大端模式下，是 0x78563412。这个例子演示了 Go 如何使用小端和大端模式将字节数组转换为 uint32 类型。\n我们通过图示再看下这个转化的对应关系：\n输出结果符合我们的预期。\n结论 # 本文主要介绍字节序这个概念，还有如何在 Go 中进行正确的字节序处理。有兴趣可阅读 encoding/binary 包的源代码，以获得更深入的理解。\n最后，希望这篇文章能对你有所帮助，如果你有任何问题，请随时提问。\n博文地址：Go 语言中如何大小端字节序？int 转 byte 是如何进行的？\n","date":"2024-02-07","externalUrl":null,"permalink":"/posts/2024-02-07-big-little-endian-in-golang/","section":"文章","summary":"嗨，大家好！我是波罗学。\n本文是系列文章 Go 技巧第十五篇，系列文章查看：Go 语言技巧。\n我们先看这样一个问题：“Go 语言中，将 byte 转换为 int 时是否涉及字节序（endianness）？我可以直接使用 int(byte_var) 进行转换吗？”\n","title":"Go 语言中如何大小端字节序？int 转 byte 是如何进行的？","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 技巧第十四篇，系列文章查看：Go 语言技巧。\n今天来聊聊在 Go 语言中是否支持三元运算符。这个问题很简单，没有。\n首先，什么是三元运算符？\n在其他一些编程语言中，如 C 语言，三元运算符是一种可以用一行代码实现条件选择的简便方法。\nx = condition ? a : b; // condition = true 则 x = a，否则 x = b 大道至简的 Go 中肯定是没有这个运算符。\n今天这篇文章将会就此展开，介绍 Go 中三元运算符的一些实践。\n让我们正式开始吧。\n使用 if-else 语句 # 三元运算符，本质上其实就是 if-else 的简化版本。通过 if-else 实现自然就是最常用的做法。\nvar x int if condition { x = a } else { x = b } 非常简单且易理解，无心智负担。毕竟，这就应该是它本来的样子。\n虽然这比三元运算符要长一些，但它更容易理解，也是 Go 所推荐的方式。\n一行表达式 # 三元运算符之所以被人喜爱，我觉得重要的一个原因就是：它足够简洁。我们只要一行代码就实现条件判断。\n在 Go 中，如果想在一行代码实现，可能吗？\n我们先来看看 rust 和 Python 是如何实现的。\n如果了解 rust，你可能看过如下代码。\nlet x = { if condition { a } else { b } }; 如上的代码中，我们创建了一个代码块，它的最后一个表达式会作为 x 的值。这是 rust 所支持的语法。其实现代的不少语言支持这种简约语法。\n或者更简洁下写法也可以，如下：\nlet = if condition {a} else {b} 如果你了解 Python，你可能看到这样的代码。\nx = a if condition else b 是不是更加简洁。\nGo 不支持这样的语法，我们要实现类似效果，就只能通过立刻执行的匿名函数实现。\n代码如下：\nx := func() int { if condition { return a } return b }() 算了，好丑，太麻烦了！\n看起来还是 if-else 好用。但我还是不甘心，还是希望实现一行代码的效果，怎么办呢？\nIf 函数 # 前面的示例中，我们通过匿名函数实现类似于三元运算符的功能。那不是说，我们预实现一个函数即可？\n让我们写一个 If 的函数来模拟三元运算符。这个函数接收一个布尔值和两个可能的返回值。根据布尔值的真假，它返回其中一个值。\n代码如下所示：\nfunc If(condition bool, a, b int) int { if condition { return a } return b } x := If(3 \u0026gt; 2, x1, x2) 现在的代码是不是就清晰了许多呢？\n但这种方法还是有个缺点，就是针对不同的类型都要实现一个 If 函数，如 IfInt()、IfString()、IfFloat() 等等。\n不过从 Go 1.18 开始，Go 成功引入泛型。\n我们可以通过泛型扩展一个更通用的 If 函数，不仅仅适用于整数，还可以用于其他类型。\n示例代码如下：\nfunc If[T any](condition bool, a, b T) T { if condition { return a } return b } func main() { x := 10 result := If(x \u0026gt; 0, \u0026#34;positive\u0026#34;, \u0026#34;negative\u0026#34;) fmt.Println(result) // 输出 \u0026#34;positive\u0026#34; } 当然，我也不是建议这么用。既然官方不支持就算了吧，if-else 多写几行就多写几行吧。\n奇淫巧技：基于 map # 在网上，我还发现了一个奇淫巧技：基于 Map 模拟三元运算法。\n代码如下：\nx = map[string]int{ true: b, false: c, }[a] 基于 true 和 false 实现条件判断。\n这方法看起来挺有创意，但这其实会增加代码的理解成本，降低可读性。再者，这种方法的效率是没有 if-else 的效率高的，因为涉及到了 map 的算法实现，没有那么直接。\n为什么 Go 没有三元运算符 # 你是否好奇，为什么 Go 语言没有三元运算符？\n官方认为三元运算符有时会让代码变得复杂和难以理解。Go 鼓励写出更清晰直接的代码。\n一个 C 语言版本的复杂三元运算符示例代码：\n#include \u0026lt;stdio.h\u0026gt; int main() { int x = 5, y = 10, z = 15; char *result; result = x \u0026gt; y ? \u0026#34;X\u0026#34; : y \u0026gt; z ? \u0026#34;Y\u0026#34; : z \u0026gt; x ? \u0026#34;Z\u0026#34; : x == y ? \u0026#34;X equals Y\u0026#34; : y == z ? \u0026#34;Y equals Z\u0026#34; : x == z ? \u0026#34;X equals Z\u0026#34; : \u0026#34;All equal\u0026#34;; printf(\u0026#34;%s\\n\u0026#34;, result); return 0; } 看这个代码，头晕没？\n我们看看摘自官方文档的原文：\nThe reason ?: is absent from Go is that the language\u0026rsquo;s designers had seen the operation used too often to create impenetrably complex expressions. The if-else form, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.\n翻译内容：\nGo 语言中没有 ?: 运算符的原因是，该语言的设计者们观察到这种运算符过于频繁地被用来创建难以理解的复杂表达式。尽管 if-else 形式更长，但它无疑更清晰。一种语言只需要一种条件控制流构造。\n从 rust 和 python 的决策上也可看出，这个观点得到了很多人的认同。但与 Go 不同的是，rust 和 python 虽然不支持传统的三元运算符，它们都提供了其他简洁的写法。\n不禁思考：Go 强调大道至简。但 rust 和 python 其实也挺简单的，依旧保留了三运算法符的优点。\n总结 # 本文主要就 Go 中三元运算符展开讨论，从简单 if-else 语句、到基于匿名函数的单行表达式、及泛型抽象 If 函数等方式来实现类似的功能。当然，我没有建议使用这些方式，在没有内置支持的情况下，if-else 的写法就挺好的。\n博文地址：Go 中如何实现三元运算符？Rust 和 Python 是怎么做的？\n","date":"2024-02-06","externalUrl":null,"permalink":"/posts/2024-02-06-ternary-operator-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 技巧第十四篇，系列文章查看：Go 语言技巧。\n今天来聊聊在 Go 语言中是否支持三元运算符。这个问题很简单，没有。\n首先，什么是三元运算符？\n","title":"Go 是否有三元运算符？Rust 和 Python 是怎么做的？","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 技巧第十三篇，系列文章查看：Go 语言技巧。\nGo 中如何检查文件是否存在呢？\n如果你用的是 Python，可通过标准库中 os.path.exists 函数实现。遗憾的是，Go 标准库没有提供这样直接的函数，好在，没有直接的，却有不那么直接的。\n本文将基于这个话题展开，介绍 Go 中如何检查文件是否存在。\n另外，本文最后还会介绍一个小注意点，即在判断文件是否存在时，如何避免中潜在的竞态条件。\nos.Stat 检查文件状态 # Go 标准库虽然没有提供类似于 os.Exist 这样直接的函数检查文件是否存在，但它提供另外一个函数 os.Stat。\nos.Stat 函数的作用是获取文件状态信息，我们通过检查它返回的错误即可知晓文件是否存在。\n示例代码，如下所示：\nfunc main() { _, err := os.Stat(\u0026#34;/path/to/file\u0026#34;) if err != nil { if os.IsNotExist(err) { // 文件不存在 } else { // 其他错误 } } // 文件存在 } 第一个返回值表示文件信息，不是我们关心的重点，直接省略掉。\n第二个返回值表示错误 error。如果文件不存在，可通过检查 os.IsNotExist 检查 error 是否是 os.ErrNotExist，确定文件是否存在。\n与 C 对比 # 上面的示例中，我们使用 os.Stat 函数获取文件的状态，通过 errors.Is 判断返回错误，如果是 os.ErrNotExist，则文件不存在。\n不得不说，这其实更底层更标准的做法。\n类似于 Python 等高级语言，提供 os.path.exist 主要是为了方便编程，提高效率。\n如果使用 Unix C 实现同样的功能，示例代码如下：\n#include \u0026lt;errno.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; int main() { struct stat buffer; int exist = stat(\u0026#34;/path/to/file\u0026#34;, \u0026amp;buffer); if (exist != 0) { if (errno == ENOENT) { /* 文件不存在*/ } else { /* 其他错误 */ } return 0; } // 文件存在 return 0; } 是不是和我们前面代码基本是一个模子。\nGo1.13 以及之后推荐使用 errors.Is # 自 Go 1.13 起，推荐使用 os.Stat 和 errors.Is 的组合。这种方法提供了更一致和灵活的错误处理方式。\n具体而言，即使是经过包裹的错误，errors.Is 依然能够识别。\n我期初认为，os.IsNotExist 能识别包裹 error，但不太确定，于是写了个代码简单测试了下。\n示例代码，如下所示：\n_, err := os.Stat(\u0026#34;/path/to/file\u0026#34;) // 这是一个不存在的文件路径 werr := fmt.Errorf(\u0026#34;Main: %w\u0026#34;, err) // 包裹生成新错误 fmt.Println(os.IsNotExist(err)) // 返回 true，表示不存在，这是错误结果 fmt.Println(os.IsNotExist(werr)) // 返回 false，表示存在 fmt.Println(errors.Is(werr, os.ErrNotExist)) // 返回 true 表示不存在 测试结果都已写在注释中。\n如上可知， os.IsNotExist 只能识别最初的 error，如果错误经过 fmt.Errorf 包裹，则必须使用 errors.Is 识别。\n一句话概括，os.IsNotExist 可以用，但有适用范围，而 errors.Is 则更通用。\n这一般也同样适用于其他类似的库。\n直接使用 Open 避免竞态条件 # 到这里，基本已经解答了 Go 中如何检查文件存在性的问题。\n但，我还想引入一个讨论：并发场景下，如何避免检查文件存在性时引入潜在的竞态条件？\n简言之，文件状态可能在检查和操作发生变化。\n什么是更好的做法呢？\n我们可以直接尝试打开或操作文件，根据返回结果判断错误。\n示例代码如下：\nfile, err := os.Open(\u0026#34;/path/to/file\u0026#34;) if err != nil { if errors.Is(err, os.ErrNotExist) { // 文件不存在 } else { // 处理其他类型的错误 } } 如上代码中，你通过 open 直接打开一个文件，如果文件不存在，os.Open 将返回一个错误，我们检查 error 确定下一步的操作。\n通过这种方式，我们可以避免打开文件时引入竞态条件。\nopen 是原子操作？ # 读到这里，可能有人不禁问，为什么 open 能避免竞态条件呢？它是原子操作吗？\n是的。\n系统调用都是原子操作，操作系统会保证操作过程不受到干扰。如果出现问题，也会进行回滚操作.\n这一点对于 Open 同样使用。\n当我们使用 open 打开一个文件时，系统会确保在这个操作完成前，不会受其他操作干扰，包括如检查文件是否存在、创建文件描述符、分配必要的资源等。\n结论 # 本文通过一个小小的问题：Go 语言中如何检查文件是否存在，除了引出 Go 中检查文件是否存在的基本方法。同时，还介绍了文件操作时如何避免潜在的竞态条件，进一步了解到一个有趣的小知识，Unix 系统调用是原子性操作。\n最后，还是希望本文能帮助各位在 GO 语言的学习道路上起到一点微末作用。\n博客地址：Go 中如何检查文件是否存在？可能产生竞态条件？\n","date":"2024-02-05","externalUrl":null,"permalink":"/posts/2024-02-05-check-if-file-exists-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 技巧第十三篇，系列文章查看：Go 语言技巧。\nGo 中如何检查文件是否存在呢？\n如果你用的是 Python，可通过标准库中 os.path.exists 函数实现。遗憾的是，Go 标准库没有提供这样直接的函数，好在，没有直接的，却有不那么直接的。\n","title":"Go 中如何检查文件是否存在？可能产生竞态条件？","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 技巧第十二篇，系列文章查看：Go 语言技巧。\n枚举，即 enum，可用于表示一组范围固定的值，它能助我们写出清晰、安全的代码。\n以编写游戏程序为一个简单案例：游戏中的角色有如战士、法师或者弓箭手，这种范围固定的值，就可以用枚举来表示。\n但 Go 中，枚举的表现方式不像在某些其他语言中那样直接。我们要想在 Go 中用好枚举，就要了解 Go 中枚举的不同表示形式和使用注意点。\n本文将以 Go 语言中如何使用枚举为主题，从最简单到复杂逐一介绍常见的方案。\n使用 iota 和常量 # 在 Go 中，使用 iota 和常量是最常见的表示枚举的方式。\n什么是 iota？\niota 是 Go 中是一个非常特别的 Keyword，它可以帮助我们按一定规则创建一系列相关的常量，而无需手动为每个变量单独赋值。这一点与枚举的用途天然契合。\n不了解上面文字的含义？\n让我们来看一个例子，基于 iota 快速创建特定规则的常量。\n示例代码，如下所示：\ntype Weekday int const ( Sunday Weekday = iota // 0 Monday // 1 Tuesday // 2 Wednesday // 3 Thursday // 4 Friday // 5 Saturday // 6 ) 例子中，Weekday 类型有 7 个值，分别代表一周的七天。内部值从 0 开始，iota 自动增加赋值给每个常量，从 Sunday 到 Saturday 分别赋值 0-7。\n现在，我们就不用手动给每个常量赋值。\niota 还有很多骚操作，本文目标不在此，就不展开了。\n这种方法的优点是简单，提供了一定程度上类型安全，但它也有局限性。\n我觉得主要是两点不足。\n首先，对比其他语言的枚举，它不能直接从字符串转换到枚举类型，以上面代码为例，它不能从 \u0026ldquo;Sunday\u0026rdquo; 字符串转为 Sunday 枚举值。\n其次，它的类型安全不是绝对安全。\n如上的 Weekday 类型，我们虽不能将一个明确类型的变量赋值给 Weekday 类型变量：\nday := 0 // int // compiler: cannot use day (variable of type int) // as Weekday value in variable declaration [IncompatibleAssign] var sunday Weekday = day 但却可以将一个非 Weekday 类的字面量赋值给它。\n// 字面量 10 赋值给类型为 Weekday 的 day 变量 var day Weekday = 10 很明显，10 这个数字并不在 Weekday 的有效范围，但却可以有效赋值而并没有报错。\n如果是其他枚举机制完善的 enum 类型的语言，肯定是无法编译通过的。\n除了最基础的实现方式，我们继续看看还有哪些其他表示形式吧。\n支持字符串转化的枚举值 # 我们在开发 Web 应用时，常会遇到要将枚举值以字符串形式表示的需求，特别是在与前端对接时。那么，就让我们先尝试实现这一个需求，string 变量与枚举变量相互转化。\n这个问题说来简单，Go 语言中，我们可采用字符串常量作为枚举值。\n示例代码，如下所示：\ntype HttpMethod string const ( Get HttpMethod = \u0026#34;GET\u0026#34; Post HttpMethod = \u0026#34;POST\u0026#34; Put HttpMethod = \u0026#34;PUT\u0026#34; Delete HttpMethod = \u0026#34;DELETE\u0026#34; ) 这种方法简单直观，而且也易于与 JSON 等数据格式转换。\ntype Request struct { Method HttpMethod URL string } func main() { r := Request{Method: Get, URL: \u0026#34;https://zhihu.com\u0026#34;} result, _ := json.Marshal(r) fmt.Printf(\u0026#34;%s\\n\u0026#34;, result) } 输出：\n{\u0026#34;Method\u0026#34;:\u0026#34;GET\u0026#34;,\u0026#34;URL\u0026#34;:\u0026#34;https://zhihu.com\u0026#34;} 当如果我们还想保留原始的 iota 的整型枚举值，毕竟它更轻量，占用内存空少。这是否可以实现呢？我们尝试一下吧。\n定义如下：\ntype HttpMethod int const ( Get HttpMethod = iota Post Put Delete ) 只要在枚举类型上增加整型值与字符串两者间相互转化的方法即可。\n代码如下所示：\n// 从 string 转为 HttpMethod func NewFromString(method string) HttpMethod { switch h { case \u0026#34;Get\u0026#34;: // 省略 ... case \u0026#34;Delete\u0026#34;: default: return Get // default is Get or error, panic } } // 从 HttpMethod 转为 string func (h HttpMethod) String() string { switch h { case Get: return \u0026#34;Get\u0026#34; // 省略 ... default: return \u0026#34;Unknown\u0026#34; // or error, panic } } 我们实现从 string 构造 enum 方法，和从 enum 类型转为 string 的 String 方法。\n这里存在的一个问题，如果希望支持友好的 JSON 序列化反序列化的话，即枚举值使用字符串形式表示，则需要为 HttpMethod 新增方法，实现 json.Marshaler 和 json.Unmarshaler 接口，自定义转化过程。\n代码如下：\n// MarshalJSON 实现 json.Marshaler 接口 func (h HttpMethod) MarshalJSON() ([]byte, error) { return json.Marshal(h.String()) } // UnmarshalJSON 实现 json.Unmarshaler 接口 func (h *HttpMethod) UnmarshalJSON(data []byte) error { var method string if err := json.Unmarshal(data, \u0026amp;method); err != nil { return err } *h = NewFromString(method) return nil } 如果去找一些开源项目，可能会发现一些实现了这种 enum 的包，你只要通过 iota 定义枚举类型，从字符串和枚举间转化的代码可通过命令直接生成。\nrobpike 开发过一个工具名为 stringer，可直接基于类似如上 HttpMethod 定义生成 String() 方法，不过它不是完整的 enum 支持。\n//go:generate stringer -type=HttpMethod type HttpMethod int const ( Get HttpMethod = iota Post Put Delete ) 我们执行 go generate 即可为 HttpMethod 类型生成 String 方法。\ngo generate 这里有个提前，要单独安装下 stringer 命令。\n不过，即使到现在，依然存在类型安全的问题，类似 var Hello HttpMethod = 10 这样的代码依然有效。\n继续吧！\n结构体枚举值 # GO 中可基于结构体类型，实现枚举效果。\n举例说明，我们创建一个颜色的枚举，要求不仅有颜色的名字，还有颜色的 RGB 值。同时，为了方便记录，我们可以给它加上一个枚举整型值。\ntype Color struct { Index int Name string RGB string } 这样我们就有了一个颜色的枚举，每个颜色都有一个索引、名字和 RGB 值。\n如何使用呢？\n类似于前面的方式，我们直接定义，如下所示：\nvar ( Red = Color{0, \u0026#34;Red\u0026#34;, \u0026#34;#FF0000\u0026#34;} Green = Color{1, \u0026#34;Green\u0026#34;, \u0026#34;#00FF00\u0026#34;} Blue = Color{2, \u0026#34;Blue\u0026#34;, \u0026#34;#0000FF\u0026#34;} ) 这种方法比较灵活，但也相对复杂。\n好处也比较明显，如现在能存储的信息也更加丰富，前面类似于整型与字符串的各种转化都变的轻而易举了。我们直接整型数值 Color.Index、字符串 Color.Name。\n不过，如果要最大化与其他库结合，如自定义 JSON 转化规则，要实现 JSON 序列和反序列接口，字符串格式化要实现 Stringer 接口等。\n还有，这种结构其实不是常量类型的，就存在数据可更改的问题。不过，有这个安全需求的话，可考虑将成员字段私有化，通过方法变更即可。\n结构体类似命名空间效果 # 在网上看到个有点傻的设计，顺便也提一下吧。\n假设，我们有很多枚举类型，担心可能会出现命名冲突，可以用结构体来创建一个“命名空间”，把相关的枚举值组织在一起：\n示例代码如下所示：\nvar Colors = struct { Red, Green, Blue Color }{ Red = Color{0, \u0026#34;Red\u0026#34;, \u0026#34;#FF0000\u0026#34;} Green = Color{1, \u0026#34;Green\u0026#34;, \u0026#34;#00FF00\u0026#34;} Blue = Color{2, \u0026#34;Blue\u0026#34;, \u0026#34;#0000FF\u0026#34;} } 上面的例子中定义了 Colors 变量，它是匿名结构体类型，字段名表示颜色，我们可通过 Colors.xxx 形式调用颜色。\n我初期看到这个写法，还以为限定了类型可定义的枚举值范围。发现其实不是，我依然可使用 Color 类型定义新值。\n这很不优雅，也很鸡肋，其实我完全可以新建 package 实现。不过既然发现了这个方案，就写到这里吧。\n类型安全？ # 到这里，其实所有实现方式都没有解决一个问题，那就是定义完枚举后，依然继续添加新的枚举值。\n我真的想实现这样的能力呢？该如何做呢？\n以前面 HttpMethod 为例，我要做的就是禁止通过 HttpMethod(1) 创建新枚举值。\n这不是很简单吗？\n我们只要将枚举实现封装成一个 package，将类型小写，如 httpMethod，暴露它的类似 FromString 和其它函数，实现强制通过转化函数它。\npackage httpmethod type httpmethod string const ( Get = \u0026#34;Get\u0026#34; Post = \u0026#34;Post\u0026#34; ) func FromString(method string) httpmethod { switch method { case \u0026#34;Get\u0026#34;: return Get case \u0026#34;Post\u0026#34;: return Post } } 现在，枚举创建必须通过方法，我们就可以在其中实现限定创建规则。\n方法可能挺好，但好像没见到这么玩的？\n为什么呢？\n我的猜想是，开发时我们不会随意创建新的枚举值，对于边界数据的传递，确保通过转化函数处理不就行了吗？\n真实场景 # 对真实场景下枚举的使用，有价值之处主要在与其他系统的对接。\n举例而言，如来自前端 API 或数据库，有时可能出现一些异常值。对这类场景，通过前面介绍，可提供转化函数，在其中设置检查规则。如果发现异常选择丢弃，执行如 error 或 panic。\n而对于内部系统，如果使用类似于 protobuffer 协议，可在协议上限定好枚举范围，标记异常数据等。\n当然，可能出现因为发布时间次序或者兄弟团队忘记通知等，导致系统间枚举值对不齐的情况，也会按上面的逻辑丢弃、error 等，便于即使发现。\n对于团队合作这类场景，最好的解决方式，还是要在设计系统时，考虑上下游的兼容性，而不是每当有变动，全员乱糟糟，这最容易导致生产事故了。\n其实无论哪一种情况，重点在于保证进入系统的数据是否可通过转化检测，而不是多此一举，限制类似于 HttpMethod(\u0026quot;Get\u0026quot;) 的类型转化，因为没有人会这么写代码。\n总结 # Go 语言中，枚举的表达方式多种多样。从简单的 iota 到复杂的结构体方式，每种方法都有其适用场景。作为开发者，最好是根据自己的具体需求，选择合适的实现方式。\n最后，希望这篇文章能帮助你在使用 Go 语言时，更加灵活且游刃有余地使用枚举 enum。\n博客地址：Go语言中 enum 实现方式有哪些？一定要类型安全吗？\n","date":"2024-02-02","externalUrl":null,"permalink":"/posts/2024-02-02-how-to-use-enum-type-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 技巧第十二篇，系列文章查看：Go 语言技巧。\n枚举，即 enum，可用于表示一组范围固定的值，它能助我们写出清晰、安全的代码。\n","title":"Go语言中 enum 实现方式有哪些？一定要绝对类型安全吗？","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 技巧第十一篇，系列文章查看：Go 语言技巧。\n不知道大家是否遇到打印结构体的需求？\n结构体的特点是，它有点像是一个盒子，但不用于 slice 与 map，它里面可以放很多不同类型的东西，如数字、字符串、slice、map 或者其他结构体。\n但，如果我们想看看盒子中内容，该怎么办呢？这时我们就要能打印结构体了。\n打印结构体的能力其实挺重要的，它能帮我们检查理解代码，提高调试效率，确保代码运行正确。\n本文让我们以此为话题，聊聊 GO 语言中如何打印结构体。本文中描述的方法基本也适用于如指针、切片、映射等深层次结构的打印。\n让我们直接开始吧！\n定义结构体 # 首先，我们来定义一个结构体，它会被接下来所有的方法用到。\n代码如下所示：\ntype Author struct { Name string Age int8 Sex string } type Article struct { ID int64 Title string Author *Author Content string } 我们定义了一个 Article 结构体用于表示一篇文章，内部包含内部实现了 ID、Title 和 Content 基础属性，还有一个字段是 Author 结构体指针，用于保存作者信息。\n我将先介绍四种基本的方式。\n使用 fmt.Printf # 最简单的方法是使用 fmt.Printf 函数，如果希望显示一些详情，可和 %+v 格式化符号配合。这样可以直接打印出结构体的字段名和值。\n我们可以这样打印它，代码如下：\nfunc main() { article := Article{ ID: 1, Title: \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: \u0026amp;Author{\u0026#34;poloxue\u0026#34;, 18, \u0026#34;male\u0026#34;}, Content: \u0026#34;This is a blog post\u0026#34;, } fmt.Printf(\u0026#34;%+v\\n\u0026#34;, article) } 输出：\n{ID:1 Title:How to Print a Structure in Golang Author:0xc0000900c0 Content:This is a blog post} 如上所示，这段代码会打印出 article 结构体的所有字段值。不过，如果仔细观察，会发现它的 Author 字段只打印了指针地址 - Author:0xc0000900c0 ，没有输出它的内容。\n这其实是符合预期的。*Author 是指针类型，它的值自然就是地址。\n如果我就想打印 Author 字段的内容，可通过用 fmt.Printf 打印指针实际指向内容。\nfmt.Print(\u0026#34;%+v\\n\u0026#34;, article.Author) 输出：\n\u0026amp;{Name:poloxue Age:18 Sex:male} 我在测试的时候，发现个有趣的现象：\n如果打印的是结构体指针，它会自动解引用，即能把内容打印出来。如上的代码所示，无论是\nPrintf(\u0026#34;%+v\\n\u0026#34;, article.Author) 还是\nPrintf(\u0026#34;%\\n\u0026#34;, *article.Author) 都能打印出结构体的内容。\n但如果打印的结构体，包含结构体指针字段，则不会将内容打印出来，而只会打印地址，即指针值。\n我猜测，如此设计的原因是为了防止深层递归，或者循环引用。\n想明白的话，似乎是个显而易见的事情。\n实现 String 方法 # 除了以上将 Author 字段单独拿出打印，我们还有其他方法实现吗？当然有，这就是本节要说的 - 实现 String 方法。\n这其实是 Go 提供的一种机制，一个类型如果满足 Stringer 接口，即实现了 String 方法，打印时返回的就是 String 方法的返回内容。\nStringer 定义如下：\ntype Stringer interface { String() string } 当我们使用 fmt.Printf 打印结构体时，就会调用定义的 String 方法，控制结构体的输出格式。例如：\nfunc (a Article) String() string { return fmt.Sprintf(\u0026#34;Title: %s, Author: %s, Content: %s\u0026#34;, a.Title, a.Author.Name, a.Content) } func main() { article := Article{ ID: 1, Title: \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: \u0026amp;Author{\u0026#34;poloxue\u0026#34;, 18, \u0026#34;male\u0026#34;}, Content: \u0026#34;This is a blog post\u0026#34;, } fmt.Println(article) } 输出：\nTitle: How to Print a Structure in Golang, Author: poloxue, Content: This is a blog post 检查结果，的确是 String 方法中定义的形式。现在，我们可以随心所欲定义打印格式了。\n这种方式还有一个特点，就是性能高。毕竟，它没有任何啰嗦，直接到拿到结果。\n到这里，我们已经有能力打印结构体了。但这里也有些缺点。\n首先，不够美观，输出结构易读性差。这不利于快速定位。\n其次，每次都要自定义输出。如果只是为了 debug 调试代码，而不是功能代码，希望有更方便方式直接打印。\njson.MarshalIndent # 首先，如何美化输出呢？\n如果你想要一个更美观的输出格式，可以使用 json.MarshalIndent 函数。这个函数会将结构体转换为 JSON 格式，且能控制缩进，使输出更易于阅读。\n示例代码，如下所示：\nimport ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { article := Article{ ID: 1, Title: \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: \u0026amp;Author{\u0026#34;poloxue\u0026#34;, 18, \u0026#34;male\u0026#34;}, Content: \u0026#34;This is a blog post\u0026#34;, } articleJSON, _ := json.MarshalIndent(article, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) fmt.Println(string(articleJSON)) } 如上的代码中，json.MarshalIndent 的第三个参数表示缩进的大小。我们看下代码的输出吧。\n输出：\n{ \u0026#34;ID\u0026#34;: 1, \u0026#34;Title\u0026#34;: \u0026#34;How to Print a Structure in Golang\u0026#34;, \u0026#34;Author\u0026#34;: { \u0026#34;Name\u0026#34;: \u0026#34;poloxue\u0026#34;, \u0026#34;Age\u0026#34;: 18, \u0026#34;Sex\u0026#34;: \u0026#34;male\u0026#34; }, \u0026#34;Content\u0026#34;: \u0026#34;This is a blog post\u0026#34; } 以这样美观的 JSON 格式打印的 Article 结构体，明显易读了许多。\nreflect 包打印复杂结构 # 如果想完全控制结构体打印，还可使用 reflect 包。它不仅仅是可以拿到 Go 变量的值，其他信息，如结构体的字段名和类型，都可轻而易举拿到。\n这也是为什么可通过 reflect 包最大粒度控制输出格式。\n示例代码，如下所示：\nimport ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) func main() { article := Article{ ID: 1, Title: \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: \u0026amp;Author{\u0026#34;poloxue\u0026#34;, 18, \u0026#34;male\u0026#34;}, Content: \u0026#34;This is a blog post\u0026#34;, } val := reflect.ValueOf(article) for i := 0; i \u0026lt; val.NumField(); i++ { field := val.Type().Field(i) fmt.Printf( \u0026#34;Type: %v, Field: %s, Value: %v\\n\u0026#34;, field.Type, field.Name, val.Field(i), ) } } 输出：\nType: int64, Field: ID, Value: 1 Type: string, Field: Title, Value: How to Print a Structure in Golang Type: *main.Author, Field: Author, Value: \u0026amp;{poloxue 18 male} Type: string, Field: Content, Value: This is a blog post 我们输出了字段类型、名称和值。\n当然，reflect 提供了灵活性，但具体的打印格式，我们就要自己按需求自行定义。前面介绍的 Printf 函数，内部实现本质上也依赖了 reflect 的实现。\n如果想要深度打印信息，即使是指针类型字段，也可通过 reflect 继续深度打印。\n代码类似于：\nif field.Type.Kind() == reflect.Pointer { fmt.Println(val.Field(i).Elem()) } 即 Elem() 继续深入它的内部。\n当然，如果你希望得到结构体类型相关信息，reflect 甚至可以在结构体没有实例化打印其类型的详情。\nfunc printStructType(t reflect.Type) { for i := 0; i \u0026lt; t.NumField(); i++ { field := t.Field(i) fmt.Printf(\u0026#34;%s: %s\\n\u0026#34;, field.Name, field.Type) } } func main() { t := reflect.TypeOf((*Article)(nil)).Elem() printStructType(t) } 核心就是那句 (*Article)(nil) 得到一个类型为 *Article 的 nil，避免了内存空间的占用。\n性能压测 # 我尝试了不同的打印方法后，也进行了一个简单的性能测试。\n测试结果如下所示：\nBenchmarkFmtPrintf-16 2631248\t447.3 ns/op BenchmarkJSONMarshalIndent-16 997448\t1016 ns/op BenchmarkCustomStringMethod-16 5135541\t225.5 ns/op BenchmarkReflection-16 2030233\t594.9 ns/op 测试结果显示，使用自定义的 String 方法是最快的，而 json.MarshalIndent 则是最慢的。\n这意味着如果你关心程序的运行速度，最好使用自定义的String方法来打印结构体。\n这里单独提醒一点，因为 fmt.Printf 的内部是使用反射，所以要能测试出 String() 自定义的效果，内部实现就不要用 fmt.Sprintf 等方法格式化字符，而是推荐使用 strconv 中的一些函数。\n示例代码：\nfunc (a Article) String() string { return \u0026#34;{ID:\u0026#34; + strconv.Itoa(int(a.ID)) + \u0026#34;, Title:\u0026#34; + a.Title + \u0026#34;,AuthorName:\u0026#34; + a.Author.Name + \u0026#34;}\u0026#34; } 这样才能真正意义上测试出 String 自定义的优势。不要套娃，最终得到用了 String 等于没用的效果。\n如果想知道为什么 strconv 更快，可阅读我之前的一篇文章：GO 中高效 int 转换 string 的方法与源码剖析\n三方库 # 前面介绍了 4 种打印结构内容的方案，。特别是第四种，提供了最大化的自由度。但缺点是要自定义，非常麻烦。\n接下来，我尝试推荐一些好用的三方库，它们将我们常用的一些模式实践成库，便于我们使用。\ngo-spew # 我们首先来看看一个叫做 go-spew 的第三方库。\n这个库提供了深度打印 Go 数据结构的功能，对于调试非常有用。它可以递归地打印出结构体的字段，即使是嵌套的结构体也能打印出来。\n例如：\nimport \u0026#34;github.com/davecgh/go-spew/spew\u0026#34; func main() { article := Article{ ID: 1, Title: \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: \u0026amp;Author{\u0026#34;poloxue\u0026#34;, 18, \u0026#34;male\u0026#34;}, Content: \u0026#34;This is a blog post\u0026#34;, } spew.Dump(article) } 这样会详细地打印出 article 结构体的所有内容。\n输出如下：\n(main.Article) { ID: (int64) 1, Title: (string) (len=34) \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: (*main.Author)(0xc000100330)({ Name: (string) (len=7) \u0026#34;poloxue\u0026#34;, Age: (int8) 18, Sex: (string) (len=4) \u0026#34;male\u0026#34; }), Content: (string) (len=19) \u0026#34;This is a blog post\u0026#34; 可以看出，上面的输出内容包含的信息非常丰富。\n如果希望自定义打印格式，可通过 spew 提供的 ConfigState 配置，如缩进，打印深度。\n示例代码：\n// 设置 spew 的配置 spewConfig := spew.ConfigState{ Indent: \u0026#34;\\t\u0026#34;, // 索引为 Tab DisableMethods: true, DisablePointerMethods: true, DisablePointerAddresses: true, MaxDepth: 1, // 设置打印深度为 1 } spewConfig.Dump(article) 输出：\n(main.Article) { ID: (int64) 1, Title: (string) (len=34) \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: (*main.Author)({ \u0026lt;max depth reached\u0026gt; }), Content: (string) (len=19) \u0026#34;This is a blog post\u0026#34; } 因为，我将打印深度配置为 1，可以看到 Author 的字段的内容是没有打印的。\n更多能力可自行探索。\npretty # 除了 go-spew，还有一些没那么强大，但也还不错的库，方便我们调试复杂数据结构，如 pretty。\nimport ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/kr/pretty\u0026#34; ) func main() { // 省略 ... fmt.Printf(\u0026#34;%# v\\n\u0026#34;, pretty.Formatter(article)) } 输出：\nmain.Article{ ID: 1, Title: \u0026#34;How to Print a Structure in Golang\u0026#34;, Author: \u0026amp;main.Author{Name:\u0026#34;poloxue\u0026#34;, Age:18, Sex:\u0026#34;male\u0026#34;}, Content: \u0026#34;This is a blog post\u0026#34;, } 输出结果为格式化的结构体输出。\n结语 # 本文主要介绍了 Go 语言中打印结构体的不同方法。我们从简单的 fmt.Printf 到使用反射，甚至是第三方库，我们是有很多选择。\n简单主题深入起来，扩展内容也可很丰富。\n关于打印结构体这个主题，还有一个部分没有谈到，就是日志如何记录类似结构体等复杂结构类型的变量，毕竟日志对于问题调试至关重要。后面有机会，可单独谈下这个主题。\n最后，希望这篇文章能帮助你在打印调试 GO 结构体等复杂结构时，不再迷茫。\n感谢阅读。\n博文地址：Go 中打印结构体？代码调试效率提升\n","date":"2024-02-01","externalUrl":null,"permalink":"/posts/2024-02-01-print-struct-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 技巧第十一篇，系列文章查看：Go 语言技巧。\n不知道大家是否遇到打印结构体的需求？\n结构体的特点是，它有点像是一个盒子，但不用于 slice 与 map，它里面可以放很多不同类型的东西，如数字、字符串、slice、map 或者其他结构体。\n","title":"Go 中如何打印结构体？代码调试效率提升","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第十篇，系列文章查看：Go 语言小技巧。\n在 Go 中，结构体主要是用于定义复杂数据类型。而 struct tag 则是附加在 struct 字段后的字符串，提供了一种方式来存储关于字段的元信息。然后，tag 在程序运行时不会直接影响程序逻辑。\n本文将会基于这个主题展开，讨论 Go 中的结构体 tag 究竟是什么？我们该如何利用它？另外，文末还提供了一个实际案例，帮助我们进一步提升对 struct tag 的理解。\nstruct tag 是什么？ # 如下是一个定义了 tag 的结构体 Person 类型。\ntype Person struct { Name string `json:\u0026#34;name\u0026#34;` Age int `json:\u0026#34;age\u0026#34;` } 例子中，json:\u0026quot;name\u0026quot;和json:\u0026quot;age\u0026quot; 就是结构体 tag。结构体 tag 的使用非常直观。你只需要在定义结构体字段后，通过反引号 `` 包裹起来的键值对形式就可定义它们。\n具体有什么用呢？ # 这个 tag 究竟有什么用呢？为何要定义它们。\n单从这个例子中来看，假设你是在 \u0026ldquo;encoding/json\u0026rdquo; 库中使用 Person 结构体，它是告诉 Go 在处理 JSON 序列化和反序列化时，字段名称的转化规则。\n让我们通过它在 \u0026ldquo;encoding/json\u0026rdquo; 的使用说明它的效果吧。\np := Person{Name: \u0026#34;John\u0026#34;, Age: 30} jsonData, err := json.Marshal(p) if err != nil { log.Println(err) } fmt.Println(string(jsonData)) 输出:\n{\u0026#34;name\u0026#34;:\u0026#34;John\u0026#34;,\u0026#34;age\u0026#34;:30} 可以看到输出的 JSON 的 key 是 name 和 age，而非 Name 和 Age。\n与其他语言对比的话，虽然 Go 的 struct tag 在某种程度上类似于 Java 的注解或 C# 的属性，但 Go 的 tag 更加简洁，并且主要通过反射机制在运行时被访问。\n这种设计反映了Go语言的哲学：简单、直接而有效。但确实也是功能有限！\n常见使用场景 # 结构体 tag 在 Go 语言中常见用途，我平时最常见有如下这些。\nJSON/XML 序列反序列化 # 如前面的介绍的案例中，通过 encoding/json 或者其他的库如 encoding/xml 库，tag 可以控制如何将结构体字段转换为 JSON 或 XML，或者如何从它们转换回来。\n数据库操作 # 在ORM（对象关系映射）库中，tag 可以定义数据库表的列名、类型或其他特性。\n如我们在使用 Gorm 时，会看到这样的定义：\ntype User struct { gorm.Model Name string `gorm:\u0026#34;type:varchar(100);unique_index\u0026#34;` Age int `gorm:\u0026#34;index:age\u0026#34;` Active bool `gorm:\u0026#34;default:true\u0026#34;` } 结构体 tag 可用于定义数据库表的列名、类型或其他特性。\n数据验证 # 在一些库中，tag 用于验证数据，例如，确保一个字段是有效的电子邮件地址。\n如下是 govalidator 使用结构体上 tag 实现定义数据验证规则的一个案例。\ntype User struct { Email string `valid:\u0026#34;email\u0026#34;` Age int `valid:\u0026#34;range(18|99)\u0026#34;` } 在这个例子中，valid tag 定义了字段的验证规则，如 email 字段值是否是有效的 email，age 字段是否满足数值在 18 到 99 之间等。\n我们只要将类型为 User 类型的变量交给 govalidator，它可以根据这些规则来验证数据，确保数据的正确性和有效性。\n示例如下：\nvalid, err := govalidator.ValidateStruct(User{Email: \u0026#34;test@example.com\u0026#34;, Age: 20}) 返回的 valid: 为 true 或 false，如果发生错误，err 提供具体的错误原因。\ntag 行为自定义 # 前面展示的都是利用标准库或三方库提供的能力，如果想自定义 tag 该如何实现？毕竟有些情况下，如果默认提供的 tag 提供的能力不满足需求，我们还是希望可以自定义 tag 的行为。\n这需要了解与理解 Go 的反射机制，它为数据处理和元信息管理提供了强大的灵活性。\n如下的示例代码：\ntype Person struct { Name string `mytag:\u0026#34;MyName\u0026#34;` } t := reflect.TypeOf(Person{}) field, _ := t.FieldByName(\u0026#34;Name\u0026#34;) fmt.Println(field.Tag.Get(\u0026#34;mytag\u0026#34;)) // 输出: MyName 在这个例子中，我们的 Person 的字段 Name 有一个自定义的 tag - mytag，我们直接通过反射就可以访问它。\n这只是简单的演示如何访问到 tag。如何使用它呢？\n这就要基于实际的场景了，当然，这通常也离不开与反射配合。下面我们来通过一个实际的例子介绍。\n案例：结构体字段访问控制 # 让我们考虑一个实际的场景：一个结构访问控制系统。\n这个系统中，我们可以根据用户的角色（如 admin、user）或者请求的来源（admin、web）控制对结构体字段的访问。具体而言，假设我定义了一个包含敏感信息的结构体，我可以使用自定义 tag 来标记每个字段的访问权限。\n是不是想到，这或许可用在 API 接口范围字段的控制上，防止泄露敏感数据给用户。\n接下来，具体看看如何做吧？\n定义结构体 # 我们首先定义一个UserProfile结构体，其中包含用户的各种信息。每个信息字段都有一个自定义的 access tag，用于标识字段访问权限（admin或user）。\ntype UserProfile struct { Username string `access:\u0026#34;user\u0026#34;` // 所有用户可见 Email string `access:\u0026#34;user\u0026#34;` // 所有用户可见 PhoneNumber string `access:\u0026#34;admin\u0026#34;` // 仅管理员可见 Address string `access:\u0026#34;admin\u0026#34;` // 仅管理员可见 } 其中，PhoneNumber 和 Address 是敏感字段，它只对 admin 角色可见。而 UserName 和 Email 则是所有用户可见。\n到此，结构体 UserProfile 定义完成。\n实现权限控制 # 接下来就是要实现一个函数，实现根据 UserProfile 定义的 access tag 决定字段内容的可见性。\n假设函数名称为 FilterFieldsByRole，它接受一个 UserProfile 类型变量和用户角色，返回内容一个过滤后的 map（由 fieldname 到 fieldvalue 组成的映射），其中只包含角色有权访问的字段。\nfunc FilterFieldsByRole(profile UserProfile, role string) map[string]string { result := make(map[string]string) val := reflect.ValueOf(profile) typ := val.Type() for i := 0; i \u0026lt; val.NumField(); i++ { field := typ.Field(i) accessTag := field.Tag.Get(\u0026#34;access\u0026#34;) if accessTag == \u0026#34;user\u0026#34; || accessTag == role { // 获取字段名称 fieldName := strings.ToLower(field.Name) // 获取字段值 fieldValue := val.Field(i).String() // 组织返回结果 result result[fieldName] = fieldValue } } return result } 权限控制的重点逻辑部分，就是 if accessTag == \u0026quot;user\u0026quot; || accessTag == role 这段判断条件。当满足条件之后，接下来要做的就是通过反射获取字段名称和值，并组织目标的 Map 类变量 result了。\n使用演示 # 让我们来使用下 FilterFieldsByRole 函数，检查下是否满足按角色访问特定的用户信息的功能。\n示例代码如下：\nfunc main() { profile := UserProfile{ Username: \u0026#34;johndoe\u0026#34;, Email: \u0026#34;johndoe@example.com\u0026#34;, PhoneNumber: \u0026#34;123-456-7890\u0026#34;, Address: \u0026#34;123 Elm St\u0026#34;, } // 假设当前用户是普通用户 userInfo := FilterFieldsByRole(profile, \u0026#34;user\u0026#34;) fmt.Println(userInfo) // 假设当前用户是管理员 adminInfo := FilterFieldsByRole(profile, \u0026#34;admin\u0026#34;) fmt.Println(adminInfo) } 输出：\nmap[username:johndoe email:johndoe@example.com] map[username:johndoe email:johndoe@example.com phonenumber:123-456-7890 address:123 Elm St] 这个场景，通过自定义结构体 tag，给予指定角色，很轻松地就实现了一个基于角色的权限控制。\n毫无疑问，这个代码更加清晰和可维护，而且具有极大灵活性、扩展性。如果想扩展更多角色，也是更加容易。\n不过还是要说明下，如果在 API 层面使用这样的能力，还是要考虑反射可能带来的性能影响。\n总结 # 这篇博文介绍了Go语言中结构体 tag 的基础知识，如是什么，如何使用。另外，还介绍了它们在不同场景下的应用。通过简单的例子和对比，我们看到了 Go 中结构体 tag 的作用。\n文章的最后，通过一个实际案例，演示了如何使用 struct tag 使我们代码更加灵活强大。虽然 struct tag 的使用非常直观，但正确地利用这些 tag 可以极大提升我们程序的功能和效率。\n博文地址：Go 中 struct tag 如何用？基于它实现字段级别的访问控制\n","date":"2024-01-28","externalUrl":null,"permalink":"/posts/2024-01-29-struct-tag-uses-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第十篇，系列文章查看：Go 语言小技巧。\n在 Go 中，结构体主要是用于定义复杂数据类型。而 struct tag 则是附加在 struct 字段后的字符串，提供了一种方式来存储关于字段的元信息。然后，tag 在程序运行时不会直接影响程序逻辑。\n","title":"Go 中 struct tag 如何用？基于它实现字段级别的访问控制","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第八篇，系列文章查看：Go 语言小技巧。\n我们编程时，常会遇到：一个函数在大多数情况下只需要几个参数，但偶尔也需要一些不固定的选项参数。在一些语言中，通过重载或者可选参数来解决这个问题。但 Go 中，情况有所不同，因为 Go 不支持函数重载，也没有内置可选参数功能。如果就想要这样的能力，如何在 Go 中实现？\n本文将基于这个主题展开，一步步介绍 GO 中实现可选参数的几种方法。\n方法1：可变长参数（Variadic Args） # GO 不支持可选参数，但它好在还是支持可变长参数，即允许函数接受任意数量的参数。这是通过在参数类型前加上 ... 来实现的。\n示例代码，如下所示：\nfunc printNumbers(numbers ...int) { for _, number := range numbers { fmt.Println(number) } } func main() { printNumbers(1, 2) printNumbers(1, 2, 3, 4) } 在上面的例子中，我们定义了一个 printNumbers 函数，它可以接受任意数量的整数作为参数。\n这种方法主要还是适合于所有参数都是同一类型的情况。\n但如果参数类型不同怎么办呢？\n当然，一种方式是，通过使用 ...interface{} 继续基于可变长参数实现，但这毫无疑问会增加反射或者类型选择或推导的开销，同时每个位置的参数按索引确定，代码复杂度必然提高，可读性会大大降低，\n那么，是否还有更好的方式呢？\n方法2：使用Map # 当你需要传递不确定数量且类型不同的参数时，可以使用 map 实现。\nfunc setConfig(configs map[string]interface{}) { if val, ok := configs[\u0026#34;timeout\u0026#34;]; ok { fmt.Println(\u0026#34;Timeout:\u0026#34;, val) } if val, ok := configs[\u0026#34;path\u0026#34;]; ok { fmt.Println(\u0026#34;Path:\u0026#34;, val) } } func main() { setConfig(map[string]interface{}{ \u0026#34;timeout\u0026#34;: 30, \u0026#34;path\u0026#34;: \u0026#34;/usr/bin\u0026#34;, }) } 在这个例子中，setConfig 函数接受一个 map 作为参数，其中键是配置项的名称，值是配置项的值。\n这种方法的缺点是失去了类型安全性，也需要在运行时对 interface{} 类型参数进行类型断言，只是相对于变长参数的方式，类型相对比较明确。\n有没有不会失去类型安全的方法呢？\n方法3：使用结构体（Structs） # 如果我们想要类型安全，同时又想要可选参数的灵活性，结构体似乎是一个不错的选择。但每次调用函数时都需要创建一个新的结构体实例，这会不会太麻烦？\ntype Config struct { Timeout int Path string } func setConfig(config Config) { fmt.Println(\u0026#34;Timeout:\u0026#34;, config.Timeout) fmt.Println(\u0026#34;Path:\u0026#34;, config.Path) } func main() { setConfig(Config{ Timeout: 30, Path: \u0026#34;/usr/bin\u0026#34;, }) } 这种方法的好处是类型安全，并且可以清晰地看到哪些参数被设置了。\n缺点是每次调用函数时都需要创建一个新的结构体实例。\n方法4：函数选项模式（Functional Options Pattern） # 那么，有没有一种方法既能保持类型安全，又能提供灵活的可选参数呢？函数选项模式似乎提供了这样的可能。\ntype Config struct { Timeout int Path string } type Option func(*Config) func WithTimeout(timeout int) Option { return func(c *Config) { c.Timeout = timeout } } func WithPath(path string) Option { return func(c *Config) { c.Path = path } } func NewConfig(opts ...Option) *Config { config := \u0026amp;Config{} for _, opt := range opts { opt(config) } return config } func main() { config := NewConfig( WithTimeout(30), WithPath(\u0026#34;/usr/bin\u0026#34;), ) fmt.Println(config) } 在这个例子中，我们定义了Config 结构体和 Option 类型，Option 是一个函数，它接受一个*Config参数。\n我们还定义了WithTimeout和WithPath函数，它们返回一个Option。这样，我们就可以在调用NewConfig函数时，通过传递不同的选项修改 Config 结构中的字段，构建不同的配置。\n这种方法的好处是非常灵活，并且可以在不破坏现有代码的情况下扩展 API。缺点是实现起来比较复杂，可能需要一些时间来理解。\n总结 # 这篇博文介绍了在Go语言中实现可选参数的几种方法：可变长参数、使用Map、结构体和函数选项模式。每种方法都有其适用场景和优缺点，你可以根据自己的需要选择合适的方法。\n博文地址：Go 语言实现可选参数：重载？变长参数？\n","date":"2024-01-27","externalUrl":null,"permalink":"/posts/2024-01-25-optional-parameters-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第八篇，系列文章查看：Go 语言小技巧。\n我们编程时，常会遇到：一个函数在大多数情况下只需要几个参数，但偶尔也需要一些不固定的选项参数。在一些语言中，通过重载或者可选参数来解决这个问题。但 Go 中，情况有所不同，因为 Go 不支持函数重载，也没有内置可选参数功能。如果就想要这样的能力，如何在 Go 中实现？\n","title":"Go 语言实现可选参数：重载？变长参数？","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第七篇，系列文章查看：Go 语言小技巧。\n这篇文章将探讨的是 Go 中如何高效使用 timer，特别是与select 一起使用时，如何防止潜在的内存泄漏问题。\n引出问题 # 先看一个例子，我们在 Go 中的 select 使用定时器，实现为消息监听加上超时能力。\n核心代码，如下所示：\nfunc main() { ch := make(chan int) // 启动一个goroutine go func() { for { select { case num := \u0026lt;-ch: fmt.Println(\u0026#34;获取到的数字是\u0026#34;, num) case \u0026lt;-time.After(2 * time.Second): fmt.Println(\u0026#34;时间到了!!!\u0026#34;) } } }() for i := 0; i \u0026lt; 5; i++ { ch \u0026lt;- i time.Sleep(1 * time.Second) } } 在这个例子中，select 语句用于监听 channel 消息和超时。然而，我要关注的重点是 timer 的行为。它是不是能达到我们预期的目标呢？为消息监听加上超时效果呢？\n检查定时器行为 # 如果运行这段代码，将会发现，如果 timer 设置为 2 秒，主循环设置 1 秒的延迟时间，timer 不会触发。\n如下是程序的运行输出：\n获取到的数字是 0 获取到的数字是 1 获取到的数字是 2 获取到的数字是 3 获取到的数字是 4 这是因为每次循环，time.After 创建都会返回一个新的定时器，产生的后果就是，每次多会重置 select 调用的时间。\n相反，如果将定时器的超时设置为 1 秒，将主循环的time.Sleep设置为 2 秒，就能触发定时器，输出 \u0026ldquo;时间到了!!!\u0026quot;。这证明了这个定时器是有效运行的。\n潜在的内存泄漏 # Go标准库文档提到，每次调用time.After都会创建一个新的定时器。然而，我们需要认真考虑一个重要问题。\n来自官方文档引用：\nThe underlying Timer is not recovered by the garbage collector until the timer fires.\n如果这些 timer 没有达到设定时间，就不会被 GC。这会导致内存泄漏。毫无疑问，如果在常驻程序中频繁使用 timer 的，内存泄漏将会日积月累。\n最佳实践 # 要高效地管理资源并避免 timer 的内存泄漏，建议使用 time.NewTimer 和 timer.Reset 组合。这种方法允许重复使用一个定时器，减少资源消耗和潜在的内存泄漏风险。\n例如，如下是使用 time.NewTimer 改进的代码示例：\n// 为定时器定义持续时间。 idleDuration := 5 * time.Minute // 使用指定的持续时间创建新的定时器。 idleDelay := time.NewTimer(idleDuration) // 确保定时器适当地停止以避免资源泄漏。 defer idleDelay.Stop() // 进入循环以处理传入的消息或基于时间的事件。 for { // 在每次循环迭代开始时重置定时器到指定的持续时间。 idleDelay.Reset(idleDuration) // 使用select等待多个通道操作。 select { // 处理传入消息的情况。 case s, ok := \u0026lt;-in: // 检查通道是否关闭。如果是，退出循环。 if !ok { return } // 处理接收到的消息`s`。 // 在这里添加相关代码来处理消息。 // 处理定时器超时的情况。 case \u0026lt;-idleDelay.C: // 增加空闲计数器或处理超时事件。 // 这通常是您会在这里添加代码来处理超时情况的地方。 idleCounter.Inc() // 处理取消或上下文过期的情况。 case \u0026lt;-ctx.Done(): // 如果上下文已完成，则退出循环。 return } } 流程如下所示：\n这里例子中演示了 Go 语言中如何正确使用和管理 timer。通过遵循 Go 标准库的建议将能产出更高效和可靠的程序。\n结论 # 本文通过一个代码案例演示了 GO 中 timer.After 可能产生的潜在内存泄漏问题。通过使用官方推荐的方案，利用重置定时器时间实现 Timer 的重复利用，避免了潜在的内存泄漏问题。\n博文地址：Go 定时器：如何避免潜在的内存泄漏陷阱\n","date":"2024-01-24","externalUrl":null,"permalink":"/posts/2024-01-24-timer-potential-leaking-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第七篇，系列文章查看：Go 语言小技巧。\n这篇文章将探讨的是 Go 中如何高效使用 timer，特别是与select 一起使用时，如何防止潜在的内存泄漏问题。\n","title":"Go 定时器：如何避免潜在的内存泄漏陷阱","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第六篇，系列文章查看：Go 语言小技巧。\nFasthttp 是一个高性能的 Golang HTTP 框架，它在设计上做了许多优化以提高性能。其中一个显著的设计选择是使用 slice 而非 map 来存储数据，尤其是在处理 HTTP headers 时。\n为什么呢？\n本文将从简单到复杂，逐步剖析为什么 Fasthttp 选择使用 slice 而非 map，并通过代码示例解释这一选择背后高性能的原因\nSlice vs Map：基本概念 # 首先，这个设计选择背后有着深思熟虑的考量，主要围绕性能优化展开。在深入探讨之前，我们需要理解 slice 和 map 在 Go 语言中的基本概念和性能特点。\nSlice：Slice 是对数组的封装，它提供了一个动态大小的、灵活的视图。Slices 的底层实际上是数组，这意味着它们的元素在内存中是连续存储的。 Map：Map 是一种无序的键值对的集合，它通过哈希表实现。Map 提供了快速的查找、添加和删除操作，但这些操作的性能并不总是稳定。 内存分配和性能 # 在高性能的应用场景中，内存分配和回收是性能的关键因素之一。Fasthttp 在这方面做了考量。\nSlice 的内存效率\n由于 slice 的元素在内存中是连续存储的，它们访问速度快，且能有效利用 CPU 缓存。此外，slice 可以通过重新切片来复用已有的数组，减少内存分配和垃圾回收的压力。\nMap 的内存开销\n相比之下，map 的内存开销较大。\n在 map 中，键和值通常是散布在内存中的，这导致 CPU 缓存利用率不高。而且，map 的增长通常涉及重新哈希和重新分配内存，这些操作在性能敏感的应用中可能成为瓶颈。\nFasthttp 中的 SliceMap # Fasthttp 选择使用自定义的 sliceMap 结构来存储键值对，而非标准的 map。\n下面是 sliceMap 的一个简化实现和它的 Add 方法：\ntype kv struct { key []byte value []byte } type sliceMap []kv func (sm *sliceMap) Add(k, v []byte) { kvs := *sm if cap(kvs) \u0026gt; len(kvs) { kvs = kvs[:len(kvs)+1] } else { kvs = append(kvs, kv{}) } kv := \u0026amp;kvs[len(kvs)-1] kv.key = append(kv.key[:0], k...) kv.value = append(kv.value[:0], v...) *sm = kvs } 在这个设计中，sliceMap 通过以下方式优化性能：\n减少内存分配\n通过在现有的 slice 上进行操作，sliceMap 尽可能地复用内存。当容量足够时，它通过重新切片 kvs = kvs[:len(kvs)+1] 来扩展 slice，避免了额外的内存分配。\n减少垃圾回收压力\n由于 slice 的元素是连续存储的，它可以更有效地被垃圾回收器处理，减少了垃圾回收的开销。而且，由于内存是复用的，垃圾回收的次数也大大减少。\n性能优化的深层原因 # Fasthttp 使用 sliceMap 而非 map 的决策不仅仅是基于内存和性能的考量，还有更深层的原因：\n存储数据特性 # 在处理 HTTP 请求时，通常 headers、query 参数或 cookies 的数量并不多。这意味着即使使用线性搜索，查找效率也不会成为性能瓶颈。\n相比之下，虽然 hash map 提供了理论上接近 O(1) 的查找效率，但实际使用中也有其开销和复杂性。\n首先，hash map 的哈希计算本身就需要时间。 其次，哈希碰撞时，hash map 要额外处理来解决碰撞，这可能涉及到链表遍历或重新哈希等操作。 这些因素在元素数量较少时可能会抵消 hash map 在查找效率上的理论优势，而 slice 则才是更优质的选择。\nCPU 预加载特性 # 由于 slice 的内存布局是连续的，它符合 CPU 缓存的工作原理，即一次性加载相邻数据。这种连续性使得 CPU 在访问一个 slice 元素后，能预加载相邻元素到缓存中，提高后续访问的速度。\n因此，顺序访问 slice 时，缓存命中率高，减少了对主内存的访问次数，从而提高了性能。\n结论 # Fasthttp 的设计选择反映了对性能细节的深入理解和精心优化。通过使用 slice 而非 map，Fasthttp 在内存分配、垃圾回收以及 CPU 缓存利用等方面实现了优化，为高性能的 HTTP 应用提供了坚实的基础。这种设计不仅仅是技术上的选择，更是对实际应用场景和性能需求的深入洞察。\n博文地址：为什么 Golang Fasthttp 选择使用 slice 而非 map 存储请求数据\n","date":"2024-01-22","externalUrl":null,"permalink":"/posts/2024-01-18-why-fasthttp-opts-for-slice-over-map/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第六篇，系列文章查看：Go 语言小技巧。\nFasthttp 是一个高性能的 Golang HTTP 框架，它在设计上做了许多优化以提高性能。其中一个显著的设计选择是使用 slice 而非 map 来存储数据，尤其是在处理 HTTP headers 时。\n","title":"为什么 Golang Fasthttp 选择使用 slice 而非 map 存储请求数据","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第九篇，系列文章查看：Go 语言小技巧。\n在 Python 中，可以使用 type(x) 获取变量 x 的类型。在 JavaScript 中，typeof x 会返回变量 x 的类型。这些操作都很直观。\n那么，在 Go 语言中，如何快速获取一个变量的类型？\n我相信很多 Go 语言初学者都会遇到这样的问题。本文将介绍 Go 中几种常用方法，用于获取 GO 变量类型。\nGo 的类型系统 # 在 Go 中，每个变量都由两部分组成：类型（type）和值（value）。\n类型是编译时的属性，它定义了变量可以存储的数据种类和对这些数据可以进行的操作。值是变量在运行时的数据。\n类型获取 # 我将介绍几种不同的获取变量类型的方式。\n使用 fmt.Printf # 最简单直接的方式，通过 fmt.Printf 的 %T 打印变量的类型。\nfunc main() { var x float64 = 3.4 fmt.Printf(\u0026#34;Type of x: %T\\n\u0026#34;, x) } 输出:\nType of x: float64 这种方式简单直接，非常适合在代码调试阶段使用。\n类型选择 # Go 中提供了类型断言检测变量类型，是 Go 语言中提供的类型检查和转换的一种方式。\n示例如下所示：\nfunc main() { var i interface{} = \u0026#34;Hello\u0026#34; // 类型断言 s, ok := i.(string) if ok { fmt.Println(s) } } 输出：\nHello 这种方式主要用于已知变量类型的情况下，将变量转化为支持的特定类型。当然，特别说明的是，这并不是强制类型转化。\n类型选择 # 类型选择与类型推断类似，也是 Go 语言中提供的类型检查和转换的一种方式。\nfunc main() { var i interface{} = \u0026#34;Hello\u0026#34; // 类型选择 switch v := i.(type) { case string: fmt.Println(v) // case int: fmt.Println(v * 2) default: fmt.Println(\u0026#34;Unknown type\u0026#34;) } } 输出:\nHello 在 GO 不支持泛型的时候，类型选择常用于与 interface{} 接口配合，实现类似泛型的函数。\n反射 reflect.TypeOf # 我们还可以通过 reflect.TypeOf 函数返回变量的类型对象 reflect.Type，它表示其参数的类型。\n对于普通类型，我们可直接通过如下代码获取类型：\nfunc main() { var x float64 = 3.4 fmt.Println(\u0026#34;Type of x:\u0026#34;, reflect.TypeOf(x)) } 输出：\nType of x: float64 对于结构体变量，要获取变量类型，示例代码如下：\ntype Person struct { Name string Age int } func main() { p := Person{\u0026#34;John Doe\u0026#34;, 30} t := reflect.TypeOf(p) fmt.Println(\u0026#34;Type of p:\u0026#34;, t) // 输出结构体的类型 // 遍历结构体中的所有字段 for i := 0; i \u0026lt; t.NumField(); i++ { field := t.Field(i) fmt.Printf(\u0026#34;Field Name: \u0026#39;%s\u0026#39;, Field Type: \u0026#39;%s\u0026#39;\\n\u0026#34;, field.Name, field.Type) } } 输出：\nType of p: main.Person Field Name: \u0026#39;Name\u0026#39;, Field Type: \u0026#39;string\u0026#39; Field Name: \u0026#39;Age\u0026#39;, Field Type: \u0026#39;int\u0026#39; 我们获取了包括其中每个字段的类型信息。\n相对于 fmt.Sprintf、类型断言和类型选择，反射在 Go 语言中提供了更多能力，如运行时检查和修改变量类型和值的能力，允许开发者动态地获取类型信息、访问结构体字段、调用方法以及操作切片和映射等，但这些操作可能会影响程序的性能。\n其他注意点 # 在 Go 中获取类型时，有一些点我们需要注意。\n错误处理 # 类型断言可能会失败，因此使用类型断言时，故而最好应使用“comma, ok”语法来避免运行时错误。\n如前面的示例中的这段代码：\ns, ok := i.(string) if ok { fmt.Println(s) } 我们可针对性采取一些措施，保证不会因为错误的类型推断导致代码异常。\n性能考量 # 反射是一个强大但代价较高的工具，但毫无疑问，它很慢。\n反射慢是因为它在运行时进行动态类型检查和间接访问内存。同时，它还涉及安全性检查等操作。这些额外的运行时，相比于直接的静态类型操作，确实是增加了开销。\n它也可能成为你系统的性能瓶颈。\n我建议在性能敏感的代码中应谨慎使用反射，或至少增加一些机制减少使用反射的次数。\n总结 # 在 Go 语言中，理解和操作类型是编写有效代码的关键。本文介绍了几种检索变量类型的方法，包括字符串格式化、reflect 包的使用，以及类型断言和类型选择。通过这些工具，你可以更好地理解和使用 Go 语言的类型系统，编写出更清晰、更有效的代码。\n博文地址：如何有效获取 Go 变量类型？探索多种方法\n","date":"2024-01-22","externalUrl":null,"permalink":"/posts/2024-01-22-get-the-type-of-object-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第九篇，系列文章查看：Go 语言小技巧。\n在 Python 中，可以使用 type(x) 获取变量 x 的类型。在 JavaScript 中，typeof x 会返回变量 x 的类型。这些操作都很直观。\n","title":"如何有效获取 Go 变量类型？探索多种方法","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第二篇，系列文章查看：Go 语言小技巧。\n为什么 Go 语言在多个 goroutine 同时访问和修改同一个 map 时，会报出 fatal 错误而不是 panic？我们该如何应对 map 的数据竞争问题呢？\n这篇文章将带你一步步了解背后的原理，并引出解决 map 并发问题的方案。\nMap 数据竞争 # 首先，什么是 Map 数据竞争。\n当两个或多个 goroutine 在没有适当同步机制的情况下，同时访问同一块数据，且至少有一个 goroutine 在修改这块数据，就会发生数据竞争。这种情况可能导致程序的行为异常，甚至崩溃。\n而 map 是 Go 中的一种常用的数据结构，提供了快速的 Key/Value 存储能力。但 Go 默认的 map 并不提供并发安全。这意味着，如果我们没有采取措施来控制 map 同步访问，如果多个 goroutine 同时对一个map进行读写操作，就可能会引发数据竞争。\nMap 数据竞争产生 fatal error # 在 Go 语言中，处理错误的方式通常是通过返回 Error 或者 panic。然而一旦程序检测到 map 的数据竞争，就会抛出 fatal 错误。而 fatal error 即意味会立刻崩溃。毫无疑问，Go 选择了更严格的处理方式。\n通过一个简单的例子演示 fatal 错误是如何被触发的：\npackage main import ( \u0026#34;sync\u0026#34; ) func main() { m := make(map[int]int) wg := sync.WaitGroup{} for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j \u0026lt; 1000; j++ { m[j] = j } }() } wg.Wait() } 这个例子中，我创建了一个map 类型变量 m，然后启动了10个 goroutine，每个 goroutine 都尝试向map中写入 1000 个键值对。由于 map 在 Go 中不是并发安全的，这将导致数据竞争。\n这个代码可能会触发如下的 fatal 错误，输出如下所示：\nfatal error: concurrent map writes goroutine 6 [running]: ... 省略 exit status 2 为什么 fatal 错误, 而非 panic? # fatal 错误让我们没有在程序运行时进行补救。我猜测这背后的原因主要是以下两点：\n立即暴露问题\n这种处理方式确保了一旦发生数据竞争，程序将立即停止运行，迫使我们直面问题，不能逃避。\n这不仅有利于我们快速发现解决并发 bug，也促使我们编写代码时，应更注重并发安全，避免发生这类问题。\n虽然，这种方式可能会导致程序在运行时突然停止，但长远来看，它有助于提高程序的健壮性和可靠性。\n防止数据腐败\n数据竞争的后果可能非常严重，尤其是在复杂的并发系统中。\n当多个goroutine不协调地访问和修改同一个map时，可能会导致map的内部状态变得不一致，甚至损坏。\n这种状态的不确定性不仅会导致程序行为异常，还可能导致难以追踪的bug。\n深入源码：map 并发检测 # 当 Go 检测到 map 的并发写入，会通过 throw 函数抛出 fatal 错误。这一过程发生在 mapassign 函数中。\n以下是简化后的 mapassign 函数的伪代码，：\nfunc mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { // 检查是否有其他goroutine正在写入map if h.flags\u0026amp;hashWriting != 0 { throw(\u0026#34;concurrent map writes\u0026#34;) } // ...其他map赋值逻辑... // 设置标志位，表示有goroutine正在写入map h.flags |= hashWriting // ...执行map的赋值逻辑... // 写入完成，清除写入标志位 h.flags \u0026amp;^= hashWriting return val } 通过这段代码，就能理解 fatal 错误是如何被触发的。对，重点就是 h.flags\u0026amp;hashWriting 这段条件判断。\n如何避免数据竞争 # 在 Go 中，最常用的并发控制机制是使用 channel 或 sync 包中的工具。另外，Go 还提供了一个并发安全的 map 类型 - sync.Map。\nsync.Mutex # 我将通过以下这段代码演示如何使用 sync.Mutex 避免数据竞争：\npackage main import ( \u0026#34;sync\u0026#34; ) func main() { m := make(map[int]int) var mu sync.Mutex wg := sync.WaitGroup{} for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j \u0026lt; 1000; j++ { mu.Lock() m[j] = j mu.Unlock() } }() } wg.Wait() } 在这个例子中，我们使用了 sync.Mutex 确保每次只有一个 goroutine 可以写入 map，从而避免数据竞争，保证程序的稳定性和正确性。\nsync.Map # 虽然 Go 的标准 map 非并发安全，但 Go 在 1.9 版本中引入了一个并发安全的 map 类型 - sync.Map，专门设计来处理并发场景下的 Key/Value 存储。\nsync.Map 有一些特别的特性，它不需要显式的锁操作来保证并发安全，为它内部已经处理好了同步机制，这可简化我们的并发编程。\n以下示例使用 sync.Map 改写了之前通过 sync.Mutex 实现的代码。\npackage main import ( \u0026#34;sync\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var sm sync.Map wg := sync.WaitGroup{} // 写入数据 for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go func(n int) { defer wg.Done() sm.Store(n, n*n) }(i) } // 读取数据 for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go func(n int) { defer wg.Done() if value, ok := sm.Load(n); ok { fmt.Printf(\u0026#34;Key: %v, Value: %v\\n\u0026#34;, n, value) } }(i) } wg.Wait() } 这个例子中，sync.Map 的 Store 方法用于存储 Key/Value，Load 方法用于读取数据。我们通过这种方式就可以在多个 goroutine 中安全地使用map，而不必担心数据竞争。\n另外，sync.Map 相对于传统的 sync.Mutext + map 的组合，除简化了并发编程，它还针对并发场景做了一些优化，如无锁读、细粒度锁机制（非锁整个 Map）等等。更多细节，可自行研究。\n总结 # 在本文中，我们探讨了 Go 语言处理 map 并发操作数据竞争情况下的处理方式。这种设计突显了 Go 对并发安全的重视。另外，通过 Go 提供的 sync.Mutex 和sync.Map 等工具，可有效避免数据竞争，确保我们构建出稳定和高效的并发应用。\n我想说，对这些机制的理解对于我们编写出健壮的Go程序是至关重要的。\n博文地址：从 fatal 错误到 sync.Map：Go中 Map 的并发策略\n","date":"2024-01-21","externalUrl":null,"permalink":"/posts/2024-01-21-fatal-error-in-concurrent-accessing-map/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第二篇，系列文章查看：Go 语言小技巧。\n为什么 Go 语言在多个 goroutine 同时访问和修改同一个 map 时，会报出 fatal 错误而不是 panic？我们该如何应对 map 的数据竞争问题呢？\n","title":"从 fatal 错误到 sync.Map：Go中 Map 的并发策略","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第一篇，系列文章查看：Go 语言小技巧。\nGo 语言 中，将整数（int）转换为字符串（string）是一项常见的操作。\n本文将从逐步介绍几种在 Go 中将 int 转换为 string 的常见方法，并重点剖析这几种方法在性能上的特点。另外，还会重点介绍 FormatInt 高效的算法实现。\n使用 strconv.Itoa # 最直接且常用的方法是使用 strconv 包中的 Itoa 函数。Itoa 是 “Integer to ASCII” 的简写，它提供了一种快速且简洁的方式实现整数到字符串之间的转换。\n示例代码如下：\npackage main import ( \u0026#34;strconv\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { i := 123 s := strconv.Itoa(i) fmt.Println(s) } strconv.Itoa 是通过直接将整数转换为其 ASCII 字符串表示形式。这个过程中尽量减少了额外的内存分配，没有复杂逻辑。\n使用 fmt.Sprintf # 另一种方法是，使用 fmt 包的 Sprintf 函数。这个方法在功能上更为强大和灵活，因为它能处理各种类型并按照指定的格式输出。\n示例代码如下：\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { i := 123 s := fmt.Sprintf(\u0026#34;%d\u0026#34;, i) fmt.Println(s) } 虽然 fmt.Sprintf 在功能上非常强大，但它的性能通常不如 strconv.Itoa。\n为什么呢？\n因为 fmt.Sprintf 内部使用了反射（reflection）确定输入值类型，并且在处理过程中涉及到更多的字符串拼接和内存分配。\n使用 strconv.FormatInt # 当需要更多控制或处理非 int 类型的整数（如 int64）时，可以使用 strconv 包的 FormatInt 函数。\npackage main import ( \u0026#34;strconv\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var i int64 = 123 s := strconv.FormatInt(i, 10) // 10 表示十进制 fmt.Println(s) } strconv.FormatInt 提供了对整数转换过程的更细粒度控制，包括 base 的选择（例如，十进制、十六进制等）。\n与 strconv.Itoa 类似，FormatInt 在性能上也非常可观，而且 FormatInt 提供了既灵活又高效的解决方案。\n如果我们查看 strconv.Itoa 源码，会发现 strconv.Itoa 其实是 strconv.FormatInt 的一个特殊情况。\n// Itoa is shorthand for FormatInt(int64(i), 10). func Itoa(i int) string { return FormatInt(int64(i), 10) } 现在 int 转 string 的高性能源码剖析，就变成了重点剖析 FormatInt。\nFormatInt 深入剖析 # 基于 Go 1.21 版本的 itoa.go 源码，我们可以深入理解 strconv 包中整数到字符串转换函数的高效实现。\nfunc FormatInt(i int64, base int) string { if fastSmalls \u0026amp;\u0026amp; 0 \u0026lt;= i \u0026amp;\u0026amp; i \u0026lt; nSmalls \u0026amp;\u0026amp; base == 10 { return small(int(i)) // 100 以内的十进制小整数，使用 small 函数转化 } _, s := formatBits(nil, uint64(i), base, i \u0026lt; 0, false) // 其他情况使用 formatBits return s } 以下是对其核心部分的详细解读，将会突出了其性能优化的关键方面，结合具体的源码实现说明。\n1. 快速路径处理小整数 # 对于常见的小整数，strconv 包提供了一个快速路径，small 函数，直接返回预先计算好的字符串，避免了运行时的计算开销。\nfunc small(i int) string { if i \u0026lt; 10 { return digits[i : i+1] } return smallsString[i*2 : i*2+2] } 对于小于 100 的十进制整数，采用这个快速实现方案，或许这也是整数转字符串的最常见使用场景吧。\nsmall 函数通过索引到 smallsString 和 digits 获取小整数的字符串表示，这个过程非常快速。 digits 和 smallsString 的值，如下所示：\nconst smallsString = \u0026#34;00010203040506070809\u0026#34; + \u0026#34;10111213141516171819\u0026#34; + \u0026#34;20212223242526272829\u0026#34; + \u0026#34;30313233343536373839\u0026#34; + \u0026#34;40414243444546474849\u0026#34; + \u0026#34;50515253545556575859\u0026#34; + \u0026#34;60616263646566676869\u0026#34; + \u0026#34;70717273747576777879\u0026#34; + \u0026#34;80818283848586878889\u0026#34; + \u0026#34;90919293949596979899\u0026#34; const digits = \u0026#34;0123456789abcdefghijklmnopqrstuvwxyz\u0026#34; 它们也就是十进制 0-99 与对应字符串的映射。\n2. formatBits 函数的高效实现 # FormatInt 最复杂的部分是 formatBits 函数，它是整数到字符串转换的核心，它针对不同的基数进行了优化。\n10进制转换的优化\n对于10进制转换，formatBits 使用了基于除法和取余的算法，并通过 smallsString 加速两位数的字符串获取。\nif base == 10 { // ... (32位系统的优化) us := uint(u) for us \u0026gt;= 100 { is := us % 100 * 2 us /= 100 i -= 2 a[i+1] = smallsString[is+1] a[i+0] = smallsString[is+0] } // ... (处理剩余的数字) } 对于 32 位系统，使用32位操作处理较大的数字，减少 64 位除法的开销。 每次处理两位数字，直接从 smallsString 获取对应的字符，避免了单独转换每一位的开销。 2的幂基数的优化\n对于基数是2的幂的情况，formatBits 使用了位操作来优化转换。\n} else if isPowerOfTwo(base) { shift := uint(bits.TrailingZeros(uint(base))) \u0026amp; 7 b := uint64(base) m := uint(base) - 1 // == 1\u0026lt;\u0026lt;shift - 1 for u \u0026gt;= b { i-- a[i] = digits[uint(u)\u0026amp;m] u \u0026gt;\u0026gt;= shift } // u \u0026lt; base i-- a[i] = digits[uint(u)] } 位操作是直接在二进制上进行，比除法和取余操作更快。 利用 2 的幂基数的特性，通过移位和掩码操作获取数字的各个位。 通用情况的处理\n对于其他基数，formatBits 使用了通用的算法，但仍然尽量减少了除法和取余操作的使用。\n} else { // general case b := uint64(base) for u \u0026gt;= b { i-- // Avoid using r = a%b in addition to q = a/b // since 64bit division and modulo operations // are calculated by runtime functions on 32bit machines. q := u / b a[i] = digits[uint(u-q*b)] u = q } 我觉得最核心的算法就是利用移位和特殊路径预置映射关系。另外，由于算法足够优秀，还避免了一些不必要内存分配。\n结论 # 将 int 转化为 string 是一个非常常见的需求。Go 语言的 strconv 包中的 int 到 string 的转换函数展示了 Go 标准库对性能的深刻理解和关注。\n通过快速处理小整数、优化的 10 进制转换算法、以及2^n 基数的特别处理，这些函数能够提供高效且稳定的性能。这些优化确保了即使在大量数据或在性能敏感的场景中，strconv 包的函数也能提供出色的性能\n博文地址：GO 中高效 int 转换 string 的方法与源码剖析\n","date":"2024-01-20","externalUrl":null,"permalink":"/posts/2024-01-20-int-to-string-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第一篇，系列文章查看：Go 语言小技巧。\nGo 语言 中，将整数（int）转换为字符串（string）是一项常见的操作。\n","title":"GO 中高效 int 转换 string 的方法与源码剖析","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第三篇，系列文章查看：Go 语言小技巧。\n在 Go 语言中，切片拼接是一项常见的操作，但如果处理不当，可能会导致性能问题或意外的副作用。\n本文将详细介绍几种高效的切片拼接方法，包括它们的优缺点以及适用场景。\n切片拼接的必要性 # 在 Go 中，切片是一种动态数组，常用于存储和处理一系列相同类型的数据。\n在实际应用中，我们经常需要将两个或多个切片合并为一个新的切片，例如在处理字符串、整数列表或自定义结构体数组时。\n这种需求促使我们探索更高效的切片拼接方法。\n基本拼接方法及其局限性 # 使用 append 函数 # 最直接的方法是使用 append 函数，它可以将一个切片的元素追加到另一个切片的末尾。\nslice1 := []int{1, 2} slice2 := []int{3, 4} result := append(slice1, slice2...) 虽然这种方法简单快捷，但它有一个局限性：当 slice1 的容量不足以容纳所有元素时，Go 会分配一个新的底层数组。这可能导致性能问题，特别是在处理大型切片时。\n高效拼接的策略 # 为了克服基本方法的局限性，我们可以采取以下策略：\n控制容量和避免副作用 # 为了避免不必要的内存分配和潜在的副作用，我们可以先检查第一个切片的容量是否足够。如果不够，可以先创建一个新的切片，确保足够的容量。\na := []int{1, 2} b := []int{3, 4, 5, 6} c := make([]int, len(a), len(a)+len(b)) copy(c, a) c = append(c, b...) 这种方法虽然代码稍长，但可以有效避免不必要的内存分配和对原始切片的影响。\n利用 Go 1.22 的新特性 # 将要发布的 1.22 版本开始，将提供了一个新的 Concat 函数，它提供了一种更简洁的方式来拼接多个切片。\na := []int{1, 2, 3} b := []int{4, 5, 6} c := slices.Concat(nil, a, b) slices 包中 Concat 的实现源码如下：\nfunc Concat[S ~[]E, E any](slices ...S) S { size := 0 for _, s := range slices { size += len(s) if size \u0026lt; 0 { panic(\u0026#34;len out of range\u0026#34;) } } newslice := Grow[S](nil, size) for _, s := range slices { newslice = append(newslice, s...) } return newslice } 这种方法不仅代码更简洁，而且内部优化了内存分配和复制操作，适用于需要高性能处理的场景。\n切片动态扩容的深入理解 # 我们来深入理解下切片的动态扩容机制吧。这对于优化切片拼接至关重要。\n元素追加的逻辑过程 # 首先，当我们不断向切片追加元素时，如果每次追加都超出了当前的容量，Go 语言的运行时环境会自动进行内存重新分配。\n这个过程涉及到创建一个新的、更大的内存空间，并将现有元素从旧空间复制到新空间，然后追加新元素。虽然这个机制保证了切片的灵活性和动态增长能力，但在处理大量数据时，频繁的内存分配和数据复制可能会成为性能瓶颈。\n内存重新分配与数据迁移 # 当切片的容量不足以容纳新元素时，Go 会执行以下步骤：\n分配新的内存空间：创建一个更大的内存空间来容纳扩展后的切片。新空间的容量通常是原来容量的两倍。 拷贝现有元素：将原切片中的元素拷贝到新的内存空间中。 追加新元素：在新的内存空间中追加新元素。 预估容量减少不必要扩容 # 故而，为了减少内存重新分配和数据迁移的性能开销，可采取以下策略：\n预估容量：在创建切片时，如果能预估到需要存储的元素数量，应该指定一个足够大的容量。\nelements := make([]int, 0, expectedSize) 批量追加：尽量一次追加多个元素，减少触发扩容的次数。\n避免不必要的扩容：在可能的情况下，先将数据收集到一个临时容器中，然后一次性追加到目标切片。\n使用缓冲区：对于频繁变化的切片，使用一个足够大的缓冲区可以有效避免频繁的内存重新分配。\n结论 # 通过深入理解 Go 切片的内存管理机制和动态扩容行为，可以更加高效地进行切片拼接操作。合理的容量规划、批量操作和缓冲区使用，不仅提高了代码的效率，还保证了程序的稳定性和可维护性。\n","date":"2024-01-17","externalUrl":null,"permalink":"/posts/2024-01-17-slice-concatenation-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第三篇，系列文章查看：Go 语言小技巧。\n在 Go 语言中，切片拼接是一项常见的操作，但如果处理不当，可能会导致性能问题或意外的副作用。\n","title":"Go 语言中高效切片拼接和 GO 1.22 提供的新方法","type":"posts"},{"content":"嗨，大家好！本文是系列文章 Go 小技巧第五篇，系列文章查看：Go 语言小技巧。\n在 Go 语言中，context 包是并发编程的核心，用于传递取消信号和请求范围的值。但其传值机制，特别是为什么不通过指针传递，而是通过接口。虽然是简单问题，但值得引发我的一些思考。\n考虑以下典型的代码片段：\npackage main import \u0026#34;context\u0026#34; func main() { ctx, cancel := context.WithCancel(context.Background()) // ... call cancel() when specified signals are triggered handle(ctx) } func handle(ctx context.Context) error { return nil } 这段代码展示了在 Go 中创建和传递 context 的简单用法。但背后的设计理念和实现细节却值得研究。\n为什么 context 是以接口的形式传递，而非指针？这不仅涉及 Go 的并发哲学，还关系到封装性、并发安全性和接口的灵活性。\n本文将简要探讨 context 包的设计和实现，着重解析其非指针传值的原因，从而揭示 Go 并发模型背后的设计智慧。\nContext 的基本结构 # 首先，如上的代码中，通过 context.WithCancel(context.Background()) 返回的是一个 context.Context 类型，而需要明确的是，context.Context 是一个接口，而不是一个具体的数据结构。\n这个接口定义了四个方法：Deadline、Done、Err 和 Value。这些方法提供了控制和访问 context 的手段。\ntype Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key interface{}) interface{} } Context 的实现和传递机制 # 在 Go 中，context 的实现是通过结构体和指针的组合完成。例如，WithCancel 函数返回的 context.Context 类型实际上是一个指向 cancelCtx 结构体的指针。\nfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, \u0026amp;c) return \u0026amp;c, func() { c.cancel(true, Canceled) } } 这里的关键在于理解 Go 语言的传值机制。\n在 Go 中，所有的函数参数都是通过值传递的。这意味着传递给函数的总是数据的副本，而不是数据本身。\n然而，当你传递一个指针时，你传递的是指针的副本，这个副本指向原始数据。因此，即使 Go 语言只有值传递，你仍然可以通过传递指针的副本来修改原始数据。\n在调用方，我们拿到 WithCancel 返回的指针，因为它的内部实现，满足 context.Context 的接口约束，能成功转为 Context 接口类型。\n为什么 Context 不直接传递指针 # 虽然 context 的某些实现（如 cancelCtx）在内部使用指针，但 context.Context 接口本身并不暴露任何指针。\n为什么要这么做呢？\n封装性\n通过将具体结构隐藏在接口后面，context 包确保了用户不能直接访问或修改内部状态，这是良好封装的标志。如其中的 timerCtx 保存了时间信息，而 valueCtx 保存了请求范围了的上下文信息，这些数据保证一致性和并发安全。\n这种设计防止了不恰当的使用，保持了 context 的行为一致性和预测性。\n并发安全\ncontext 被设计为并发安全的。如果 context 通过指针传递，暴露内部实现，那么在并发访问时，可能就有方式修改实际数据的内部状态。\n通过接口隐藏实现细节，context 的设计者可以确保内部状态的同步和一致性，而不需要用户介入。\n灵活性\ncontext.Context 是一个接口，这意味着你可以有多种实现。\n如果 context 通过指针传递，那么所有的实现都必须是具体的结构体，如 handle 函数指定传递 cancelCtx 的话，那就不能传递 timerCtx、valueCtx 等其他类型 Context 实现类。而通过使用接口，Go 语言允许更多的灵活性和实现多样性。\n我们已经完成了 context 包设计理念的探讨，尤其是它如何通过接口和封装来保证并发安全性，同时提供清晰的抽象。\n最后，让我们通过一个具体的例子来展示 Go 语言的这种设计模型。\n案例：DataStore # 我们要实现一个 datastore 实现存储数据，要求同时提供两种版本的实现：并发安全和无并发安全版本。\n代码片段如下所示，它展示了 DataStore 接口的两种不同实现：safeDataStore 和 inMemoryDataStore。\nDataStore 是一个接口，定义了对数据的操作。\nDataStore 是一个接口的具体代码，如下所示：\ntype DataStore interface { ReadData() string WriteData(data string) } safeDataStore 是一个实现了 DataStore 接口的结构体，它使用 sync.Mutex 来保证并发安全.\ntype safeDataStore struct { mu sync.Mutex data string } func (ds *safeDataStore) ReadData() string { ds.mu.Lock() defer ds.mu.Unlock() return ds.data } func (ds *safeDataStore) WriteData(data string) { ds.mu.Lock() defer ds.mu.Unlock() ds.data = data } inMemoryDataStore 是另一个实现了 DataStore 接口的结构体，它假设只在单个 goroutine 中使用，因此不需要同步机制\ntype inMemoryDataStore struct { data string } func (ds *inMemoryDataStore) ReadData() string { return ds.data } func (ds *inMemoryDataStore) WriteData(data string) { ds.data = data } 如上的代码所示，DataStore 接口定义了数据存储的基本操作。同时，我们提供了两种实现：\nsafeDataStore 使用互斥锁来保证并发安全，适用于并发场景； inMemoryDataStore 只在单 goroutine 中使用，不涉及任何同步机制，适用于简单场景。 使用这两个实现的代码如下：\nfunc main() { var store DataStore // 使用 safeDataStore store = \u0026amp;safeDataStore{} store.WriteData(\u0026#34;safe data\u0026#34;) fmt.Println(store.ReadData()) // 使用 inMemoryDataStore store = \u0026amp;inMemoryDataStore{} store.WriteData(\u0026#34;in-memory data\u0026#34;) fmt.Println(store.ReadData()) } 通过这个例子，可以看到 Go 语言是如何通过这种模式来支持多样性和灵活性的。不同的 DataStore 实现可以有不同的内部行为和性能特性，但它们对外提供了统一接口。这种设计不仅使代码模块化和易于维护，而且更加易于扩展性。\n结论 # 总结来说，无论是在 context 包的设计中，还是在我们的 DataStore 示例中，Go 语言的接口和封装都展现了其在构建并发安全且易于维护的系统方面的强大能力。通过这些机制，Go 语言为开发者提供了一种清晰、一致且灵活的方式来管理和传递程序的状态和行为。\n博文地址：从 Context 看 Go 设计模式：接口、封装和并发控制\n","date":"2024-01-16","externalUrl":null,"permalink":"/posts/2024-01-16-context-design-pattern-in-golang/","section":"文章","summary":"嗨，大家好！本文是系列文章 Go 小技巧第五篇，系列文章查看：Go 语言小技巧。\n在 Go 语言中，context 包是并发编程的核心，用于传递取消信号和请求范围的值。但其传值机制，特别是为什么不通过指针传递，而是通过接口。虽然是简单问题，但值得引发我的一些思考。\n","title":"从 Context 看 Go 设计模式：接口、封装和并发控制","type":"posts"},{"content":"本文只是我的一些尝试，基于 ChatGPT 实现系统化快速搜索某编程语言的特定领域相关包或者基于其他语言类推荐落地方案的尝试。\n这篇文章中描述的方式不一定是好方式，但应该会有一定的启示作用吧。让 ChatGPT 为我们的开发效率添砖加瓦。\n基础思路 # 在学习和使用一门新的编程语言的过程中，找到合适的包对于解决特定问题至关重要。\n传统上，还是主要依赖搜索引擎和社区资源来寻找这些包，但这个过程往往既耗时又充满挑战。现在，有了 ChatGPT，这一切都有了新的解决方案。\n传统的搜索方法通常基于关键词搜索，但这种方法往往返回只是简单列举，我们要从大量不相关的结果找出我们想要的内容，需要花费大量时间去筛选。\nChatGPT 提供了一种更高效的方式：通过与 AI 对话来精确描述我们的需求，从而获得更加精准回答。不过我想说，不要像利用传统的搜索引擎一样利用 ChatGPT，ChatGPT 会思考。但毫无疑问，再聪明的好苗子，也是需要引导的。\n举一反三全面了解 GO 的 Web 框架 # 为了最大化 ChatGPT 的效用，我采用固定句式来描述我的需求，这么做的目标主要是为了引导它思考。\n例如，当寻找 Golang 的 web 开发框架时，我不是简单地问 “给我推荐一些 web 开发框架”，而是通过类比的方式地询问，想全访问了解不同情况下使用什么 HTTP 框架。\n我知道在使用 Golang 的数据库时，可以直接使用 sql/database + 驱动连接数据库，也可以使用 ORM 方案，如 sqlx 或 GORM。HTTP 一般也是如此，有底层核心层，上层的用户体验较好的框架。\n我按这个思考简单组织了一些语言。如下所示：\n按我这个句式说下关于 Go 的 xxx 相关内容: Go 语言中，处理数据库的包通常分为三个主要层次：底层实现、数据库驱动以及上层的 ORM（对象关系映射）框架。首先，底层核心包是 sql/database，它为数据库操作提供了基础的接口和功能。接着，针对不同数据库类型的驱动包各不相同，例如，用于 MySQL 的驱动包是 go-sql-driver/mysql。最后，在更高的层面上，有基于如 sqlbuilder 或 activerecord 模式的实现，Go 语言中的相应包包括 ent 和 gorm，这些包提供了更高级别的数据库操作和抽象。\n这个写的稍微有点长，应该可以组织的更加精简一点。我主要想表达：\n通过这种方式，引导 ChatGPT 去像人来一样地思考，举一反三，深入地理解需求。而不是像以前的 AI 一样，机械呆滞地返回固定问题的答案。\n有些时候，语言直接表达的效果是没有这种类比效果好的。不仅仅这种方式适用于 ChatGPT，如果你和人类这样对话，也是更加容易让对方理解。\n我就是通过这个方式，比较全方位了解 Go 语言不同场景下 Web 框架包的。如下是我具体的问题：\n按我这个句式推荐下 Go 的 Web 框架。[句式如上，省略]*\nGPT 的回答如下所示：\n在 Go 语言中，HTTP 框架的组织也可以分为几个主要层次，类似于处理数据库的包的结构。首先，底层核心包是 net/http，提供了构建 HTTP 服务器和客户端的基本工具，如路由、请求处理和发送响应。接下来，针对不同需求的 HTTP 中间件和路由工具，例如 Gorilla Mux 提供了高级的路由功能，而 Negroni 提供了易用的中间件管理。在更高的层次上，存在着基于如 mvc 或 restful 模式的全功能 Web 框架，如 Go 语言中的 Gin 和 Echo，它们提供了路由、中间件、模板渲染等更丰富的功能，以支持复杂的 Web 应用开发。此外，还有针对特定性能优化的 HTTP 工具，例如 Fasthttp，它专注于提供高性能的 HTTP 服务，适用于处理大量并发请求。这样的层次划分帮助开发者根据具体需求选择合适的 Go 语言 HTTP 框架和库。\n系统方案建议 - 让 ChatGPT 推断 # 类似的，我这里再举个例子。针对同一个问题，可能实现思路是一样的，但实现的工具就具体情况而异。这时候，我们可以通过提供对这个问题的一种方案，让 ChatGPT 衍生，提供我们想要的另一个场景条件的实施方案。\n一个例子，比如我们习惯于使用 GO 开发 web 项目，也了解如何实施。但是现在公司希望转入 Python 或者 rust，如何最快的找到对标的实现呢？\n我们可以说：\n在 Go Web 开发时，要用到的库有：Web 框架使用 gin、 orm 使用 gorm，用户权限认证使用 golang-jwt/jwt，依次类推 python 开发 Web 项目的推荐库\nChatGPT 返回内容：\n对于Go Web开发，您提到的库选择是很不错的。以下是一些在Python Web项目中常用的库的推荐：\nWeb框架：\nFlask：Flask是一个轻量级的Python Web框架，适用于小型到中型的项目。 Django：Django是一个功能强大的Python Web框架，适用于大型项目和快速开发。 ORM（对象关系映射）：\nSQLAlchemy：SQLAlchemy是一个流行的Python ORM库，它提供了多种数据库后端的支持，并具有强大的查询和模型定义功能。 用户权限认证：\nFlask-Login：用于用户会话管理和身份验证的Flask扩展。 Django-Allauth：Django的全功能身份验证和账户管理解决方案。 JWT（JSON Web Tokens）：\nPyJWT：PyJWT是Python中处理JWT的库，可以用于用户身份验证和授权。 这些库在Python Web开发中广泛使用，可以根据项目的需求选择合适的库。希望这些推荐对您有所帮助。如果您需要更多信息或有其他问题，请随时提问。\n它这里面还多了一部分关于权限认证，可能还是我的问题提的有点瑕疵，或者 chatgpt 自己的问题。反正是，多结果总觉少结果要好。\nrust 是不熟悉的语言，我最近在尝试学习，同样的思路，还可以问 rust 的 web 开发套件。\nchatGPT 回答如下：\n当使用Rust进行Web项目开发时，以下是一些推荐的库和框架：\nWeb框架：\nActix-web：Actix-web是一个高性能的、基于异步的Web框架，适用于构建快速、可伸缩的Web应用程序。 Rocket：Rocket是一个易于使用的Web框架，它提供了类型安全的路由和请求/响应处理。 ORM（对象关系映射）：\nDiesel：Diesel是Rust的ORM库，它提供了类型安全的数据库查询和模型定义功能。 sqlx：sqlx是一个异步SQL库，允许您使用Rust进行数据库操作。 用户权限认证：\nKeycloak：Keycloak是一个开源的身份和访问管理解决方案，可以用于用户身份验证和授权。 Auth0：Auth0是一个身份验证和授权服务，可以轻松集成到Rust应用程序中。 JWT（JSON Web Tokens）：\njsonwebtoken：jsonwebtoken是Rust中处理JWT的库，可用于身份验证和令牌生成。 这些库和框架可以帮助您构建高性能、安全的Rust Web应用程序。根据项目的需求和您的偏好，选择适合的库和框架进行开发。如果您需要更多信息或有其他问题，请随时提问。\n不知道这个答案质量，你觉得如何呢？毫无疑问，这个答案，吊打搜索引擎。我这里只是简单举例，你还可以提供更复杂的需求，让它提供更多的推荐。\n我的一些微末思考 # 通过上面的这些案例，我主要想说 ChatGPT 能像人类一样思考，但它的思考的模式是我们提供的。\n这里面有一个非常简单的道理，如果你把 ChatGPT 想象是一个真正的人类，即使这个人类一个非常牛逼的大佬，但是如果他不是从教育你的角度出发，就不会说一些无关内容，一些的互动都是基于你的主动性，你的输入。如果你的问的简单，它必然是回答的简单。\n同理，如果你想让 ChatGPT 思考，你需要告诉它如何思考。这就是你要提供的信息，这个信息最能体现你的个人能力了。ChatGPT 能思考，但是基于的内容是你的输入的扩展。\n结论 # 我想说 ChatGPT 不仅是一个对话工具，更是一个强大的技术资源搜索助手，或者说，它可以成为你的朋友，导师，关键在于你是否了解如何与它交流，让它如何去做。\n它改变了寻找技术解决方案的方式。我通过简单的对话，让它思考推导，而不是像以前的智障 AI 或搜索引擎一样针对问题固定返回某个回答。\n这种结合 AI 技术的搜索方法不仅节省了我们的时间，还为我们提供了更加精确和深入的技术见解，而非是简单的列举。\n博文地址：利用 ChatGPT 高效搜索：举一反三的思考方式，高效查找解决方案\n","date":"2024-01-15","externalUrl":null,"permalink":"/posts/2024-01-15-search-using-chatgpt/","section":"文章","summary":"本文只是我的一些尝试，基于 ChatGPT 实现系统化快速搜索某编程语言的特定领域相关包或者基于其他语言类推荐落地方案的尝试。\n这篇文章中描述的方式不一定是好方式，但应该会有一定的启示作用吧。让 ChatGPT 为我们的开发效率添砖加瓦。\n","title":"利用 ChatGPT 高效搜索：举一反三的思考方式，高效查找解决方案","type":"posts"},{"content":"今天，介绍下 MoviePy 的混合剪辑 - mixing clips，即如何将多个 clip 合并为一个 clip 生成最终视频。\n前言概述 # 混合剪辑，主要指的是如何将多个 Clip 合成为一个 Clip。Clip 的类型可以是 VideoClip、或者是 AudioClip。\nMoviePy 中的混合剪辑 mixing clips 提供的方法，我大致上分三类，即是基于时间的 concantenate_clips、基于空间 clips_array 和随意合成 CompositeClips。不过，因为 audio 音频没有空间的概念，clips_array 只适用于 VideoClip 对象。\nOk, 那让我们具体介绍具体的使用吧。\n基于时间 - concatenate clips # 首先，我们介绍 concatenate clips 基于时间的视频合成。所谓，基于时间，即按时间依次播放片段。\n视频合成 # 假设，我们要制作一个视频，依次播放 \u0026ldquo;1st second\u0026rdquo;、\u0026ldquo;2nd second\u0026rdquo; 和 \u0026ldquo;3rd second\u0026rdquo;。我们可以通过 TextClip 依次创建三个视频片段，然后通过 concantenate_video_clip 合并。\n实现代码如下所示：\nfrom moviepy.editor import * clip1 = TextClip(\u0026#34;1st second\u0026#34;, color=\u0026#34;white\u0026#34;, size=(1280, 720)).set_duration(1).set_fps(1) clip2 = TextClip(\u0026#34;2nd second\u0026#34;, color=\u0026#34;white\u0026#34;, size=(1280, 720)).set_duration(1).set_fps(1) clip3 = TextClip(\u0026#34;3rd second\u0026#34;, color=\u0026#34;white\u0026#34;, size=(1280, 720)).set_duration(1).set_fps(1) video = concatenate_videoclips([clip1, clip2, clip3]) video.write_videofile(\u0026#34;concatenate_videoclips.mp4\u0026#34;) 视频如下所示，按时间依次播放片段。\n音频合成 # 如果希望基于时间合成音频，直接使用 concantenate_audio_clips 方法，如假设我当前有 \u0026ldquo;1st-second.wav\u0026rdquo;, \u0026ldquo;2nd-second.wav\u0026rdquo;, \u0026ldquo;3rd-second.wav\u0026rdquo; 音频文件。\n如下代码即可将代码按时间合成。\nclip1 = AudioFileClip(\u0026#34;1st-second.wav\u0026#34;) clip2 = AudioFileClip(\u0026#34;2nd-second.wav\u0026#34;) clip3 = AudioFileClip(\u0026#34;3rd-second.wav\u0026#34;) audio = concatenate_audioclips([clip1, clip2, clip3]) audio.write_audiofile(\u0026#34;concatenate_audioclips.wav\u0026#34;) 最终生成的音频文件效果，将依次朗读 \u0026ldquo;1st second\u0026rdquo;，\u0026ldquo;2nd second\u0026rdquo;、\u0026ldquo;3rd second\u0026rdquo;。\n基于空间 - clips_array # 如果想让多个 Clip 在同一时刻显示且平铺开，直接使用 clips_array，按空间平铺多个 clip。\n说明：音频不存在空间概念，故而 clips_array 只适用于操作视频。\n我们还是使用 TextClip 创建 Clip，然后使用 clips_array 按数组拼接起来。\n实现代码，如下所示：\nfrom moviepy.editor import * clip1 = TextClip(\u0026#34;1st second\u0026#34;, color=\u0026#34;white\u0026#34;, bg_color=\u0026#34;gray\u0026#34;, size=(1280, 720)).set_duration(1).set_fps(1) clip2 = TextClip(\u0026#34;2nd second\u0026#34;, color=\u0026#34;white\u0026#34;, bg_color=\u0026#34;gray\u0026#34;, size=(1280, 720)).set_duration(1).set_fps(1) clip3 = TextClip(\u0026#34;3rd second\u0026#34;, color=\u0026#34;white\u0026#34;, bg_color=\u0026#34;gray\u0026#34;, size=(1280, 720)).set_duration(1).set_fps(1) clip4 = TextClip(\u0026#34;4th second\u0026#34;, color=\u0026#34;white\u0026#34;, bg_color=\u0026#34;gray\u0026#34;, size=(1280, 720)).set_duration(1).set_fps(1) clip1 = clip1.margin(10) clip2 = clip2.margin(10) clip3 = clip3.margin(10) clip4 = clip4.margin(10) video = clips_array([[clip1, clip2], [clip3, clip4]]) video.write_videofile(\u0026#34;clips_array.mp4\u0026#34;) clips_array 的参数是一个二维数组，假设有一个 2 x 2 的数组，位置是：\n[ [行1列1, 行1列2], [行2列1, 行2列2] ] 我们最终合成视频的效果如下所示：\n自由合成 - composite clips # 无论是基于空间，还是基于时间，都是基于一个固定的模式合成。如果不希望局限于这种模式，就要使用 Composite Clips 类，自由合成。\n视频合成 # 我们使用 moviepy 的 CompositeVideoClips 类可随意合成视频。\n自由合成多个 Clip 的话，即某个 Clip 出现的时间和空间和其他 Clip 无关，意味我们要单独定义视频位置与播放的时间区域。\n设置位置 # 如何设置视频 Clip 位置呢？\nVideoClip 提供了一个 set_position 方法，用它即可设置视频的位置，参数是 x y 坐标点。\n坐标系如何确认呢？\n使用 ConcompositeVideoClip 合成的 Clip，大小 size 默认是参数 clips 数组的第一个 Clip 的 size，而位置的 x 与 y 便是基于这第一个 Clip 而定的。\n坐标系的原点位于左上角的位置，x 轴的方式从左向右，而 y 轴的方向是从上到下，如下图所示：\n现在，我们尝试下，目标是将一个 TextClip 添加一个视频上，居中显示视频标题。\n实现代码，如下所示：\ntitle = \u0026#34;MoviePy Basic Usage\u0026#34; video = VideoFileClip(\u0026#34;input_video2.mp4\u0026#34;) txt_clip = TextClip(title, fontsize=80, color=\u0026#34;orange\u0026#34;).set_duration(video.duration) # 计算居中坐标位置 width, height = video.size txt_width, txt_height = txt_clip.size x = (width - txt_width) /2 y = (height - txt_height) / 2 position = (x, y) txt_clip = txt_clip.set_position(position) video = CompositeVideoClip([video, txt_clip]) video.write_videofile(\u0026#34;CompositeVideoClip.mp4\u0026#34;) 最终效果如下所示：\n示例代码中，有一串代码用于计算居中的位置。对于常见的对齐方式，如居中，在 moviepy 可直接使用 \u0026ldquo;center\u0026rdquo; 指定，其他 left，right, top, bottom 也是如此。\n可通过如下代码直接将 txt_clip 居中。\ntxt_clip = txt_clip.set_position((\u0026#34;center\u0026#34;, \u0026#34;center\u0026#34;)) 设置出现时间段 # 本段介绍下如何配置 Clip 的显示时间，其核心是三个函数，set_start、set_end 和 set_duration，分别用于设置 Clip 的开始时间、结束时间和时间长度。\n一个案例，假设我们想在视频添加另一个片段，依次显示 \u0026ldquo;3\u0026rdquo;、\u0026ldquo;2\u0026rdquo;、\u0026ldquo;1\u0026rdquo;，实现倒计时的效果。\nfrom moviepy.editor import * video = VideoFileClip(\u0026#34;input_video2.mp4\u0026#34;) txt_clips = [] start = 0 duration = 1 for text in range(3, 0, -1): txt_clip = TextClip(f\u0026#34;{text}\u0026#34;, color=\u0026#34;white\u0026#34;, fontsize=128).set_start(start).set_duration(duration).set_fps(1) txt_clip = txt_clip.set_position((\u0026#34;center\u0026#34;, \u0026#34;center\u0026#34;)) txt_clips.append(txt_clip) start += duration video = CompositeVideoClip([video] + txt_clips) 通过 for 循环依次生成 TextClip 并设置每个 Clip 的显示时间段。\n效果如下所示：\n音频合成 # 我们使用 CompositeAudioClip 类合成音频。常见的场景是，我们想在原视频的基础上，添加背景音乐。\n由于，音频不存在位置的问题，就简单了很多，我们主要关注如何时间设置即可。\n一个案例，我们有三个音频文件，分别 1st-second.wav、2nd-second.wav 和 3rd-second.wav，我们使用 concatenate_videoclips 依次播放，且在 1-5 秒加上背景因为。\n演示代码如下所示：\nfrom moviepy.editor import * clip1 = AudioFileClip(\u0026#34;./1st-second.wav\u0026#34;) clip2 = AudioFileClip(\u0026#34;./2nd-second.wav\u0026#34;) clip3 = AudioFileClip(\u0026#34;./3rd-second.wav\u0026#34;) bg_music = AudioFileClip(\u0026#34;./bg-music.mp3\u0026#34;) clip1 = clip1.set_start(0).set_duration(1) clip2 = clip2.set_start(1).set_duration(1) clip3 = clip3.set_start(2).set_duration(1) bg_music = bg_music.set_start(1).set_duration(4) clips = [clip1, clip2, clip3, bg_music] audio = CompositeAudioClip(clips).set_fps(44100) audio.write_audiofile(\u0026#34;CompositeAudioClip.mp3\u0026#34;) 如此，我们就生成了一个带有背景音乐的音频文件。\n特别说明 # Clip 的合成要在相同类型 Clip 上进行，怎么理解？\n简单来说，假设要在视频上添加音频，直接通过 set_audio 添加，视频上添加蒙层 mask 使用 set_mask 添加即可，而不是使用 concantenate_clips 或者 CompositeClips 实现。\n总结 # 本博文详细介绍了 MoviePy 的各种混合剪辑方法，这块内容算是 MoviePy 中比较难理解的内容了，希望帮助大家更好地理解和使用这些功能。\n","date":"2024-01-06","externalUrl":null,"permalink":"/posts/2024-01-06-moviepy-mixing-clips/","section":"文章","summary":"今天，介绍下 MoviePy 的混合剪辑 - mixing clips，即如何将多个 clip 合并为一个 clip 生成最终视频。\n前言概述 # 混合剪辑，主要指的是如何将多个 Clip 合成为一个 Clip。Clip 的类型可以是 VideoClip、或者是 AudioClip。\n","title":"Python 视频剪辑库 - Moviepy 的混合剪辑 mixing clips","type":"posts"},{"content":"OpenAI 因为发布了 ChatGPT 这款大模型产品，这两年是异常的火啊。ChatGPT 和 Sora 是自媒体上最常听到的 OpenAI 推出的两款产品。 除此以外，OpenAI 还有文生图的 Doll-E 模型，语音转文字的 Whisper，文字转语音的 OpenAI TTS。\n本文将基于 OpenAI 语音转文字的 Whisper 实现视频自动添加字幕的能力。\n前言概述 # 编辑视频时，为视频添加字幕不仅仅可以让内容易于理解性，而且可以增加视频的吸引力。\n于视频剪辑而言，给视频添加字幕是一个常见的需求，而 python 的三方库 moviepy 配合 Openai Whisper，我们可以轻松实现这个需求。\nPython 提供了 MoviePy 库，同时近期开源的 openai-whisper 模块使得为视频添加字幕变得十分简单。\nwhisper 的安装直接通过如下命令即可完成。\npip install openai-whisper 而 moviepy 的安装可参考我之前的文章：Python 视频剪辑库 - MoviePy 基础使用。\n步骤介绍 # 添加字幕的核心步骤主要三步，如下所示：\n利用 openai-whisper 从视频中将语音识别为文本； 基于识别结果生成 moviepy 字幕片段 SubtitlesClip； 将字幕片段 SubtitlesClip 和视频片段 VideoFileClip 合成最终文件； 让我们进入开始具体的操作。\n语音识别 # 我们现在在当前目录有一个视频，名为 input_video2.mp4。可直接利用 whisper 的 transcribe 识别视频文件中的语音文本。\n实现代码如下：\nimport whisper model = whisper.load(\u0026#34;medium\u0026#34;) result = model.transcribe(\u0026#34;input_video2.mp4\u0026#34;) print(json.dumps(result)) 输出 result，如下所示\n{ \u0026#34;text\u0026#34;: \u0026#34; In an era dominated by short-form videos, numerous user-friendly editing software options exist. However, for specialized video formats like e-book presentations or comic readings, streamlining the video creation process through automation stands as a crucial enhancement for efficiency.\u0026#34;, \u0026#34;segments\u0026#34;: [ { \u0026#34;id\u0026#34;: 0, \u0026#34;seek\u0026#34;: 0, \u0026#34;start\u0026#34;: 0.0, \u0026#34;end\u0026#34;: 6.7, \u0026#34;text\u0026#34;: \u0026#34; In an era dominated by short-form videos, numerous user-friendly editing software options exist.\u0026#34;, \u0026#34;tokens\u0026#34;: [], \u0026#34;temperature\u0026#34;: 0.0, \u0026#34;avg_logprob\u0026#34;: -0.170647520768015, \u0026#34;compression_ratio\u0026#34;: 1.4947916666666667, \u0026#34;no_speech_prob\u0026#34;: 0.007638249080628157 }, { \u0026#34;id\u0026#34;: 1, \u0026#34;seek\u0026#34;: 0, \u0026#34;start\u0026#34;: 6.7, \u0026#34;end\u0026#34;: 12.0, \u0026#34;text\u0026#34;: \u0026#34; However, for specialized video formats like e-book presentations or comic readings,\u0026#34;, \u0026#34;tokens\u0026#34;: [], \u0026#34;temperature\u0026#34;: 0.0, \u0026#34;avg_logprob\u0026#34;: -0.170647520768015, \u0026#34;compression_ratio\u0026#34;: 1.4947916666666667, \u0026#34;no_speech_prob\u0026#34;: 0.007638249080628157 }, { \u0026#34;id\u0026#34;: 2, \u0026#34;seek\u0026#34;: 0, \u0026#34;start\u0026#34;: 12.0, \u0026#34;end\u0026#34;: 19.0, \u0026#34;text\u0026#34;: \u0026#34; streamlining the video creation process through automation stands as a crucial enhancement for efficiency.\u0026#34;, \u0026#34;tokens\u0026#34;: [], \u0026#34;temperature\u0026#34;: 0.0, \u0026#34;avg_logprob\u0026#34;: -0.170647520768015, \u0026#34;compression_ratio\u0026#34;: 1.4947916666666667, \u0026#34;no_speech_prob\u0026#34;: 0.007638249080628157 } ], \u0026#34;language\u0026#34;: \u0026#34;en\u0026#34; } 可以看到，result 中包含一个 segments 的数组，start 表示开始时间，end 表示结束时间，而 text 就是字幕文本。\n好了，现在我们就有了制作字幕所需的所有信息。\n字幕片段 # 接下来，让我们利用 whisper 的 result 生成字幕所需的文本片段 TextClip。\n制作字幕片段一般方法是，循环遍历 segments 制作 clips 数组，使用 set_start 和 set_end 设置 TextClip 的开始结束时间。\nsubtitle_clips = [] for segment in segments: subtitle_clip = TextClip(segment[\u0026#39;text\u0026#39;]).set_start(segment[\u0026#39;start\u0026#39;]).set_end(segment[\u0026#39;end\u0026#39;]) subtitle_clips.append(subtitle_clip) moviepy 提供一个 moviepy.video.tools.subtitles.SubtitlesClip 的类。\n我们只需将 segments 转为 [((start, end), text)] 的数组，传递给 SubtitlesClip，即可创建出 SubtitlesClip。\n代码如下所示：\nfrom moviepy.video.tools.subtitles import SubtitlesClip subtitles = [] for segment in segments: subtitle = (segment[\u0026#39;start\u0026#39;], segment[\u0026#39;end\u0026#39;]), segment[\u0026#39;text\u0026#39;] subtitles.append(subtitle) subtitles_clip = SubtitlesClip(subtitles) OK，现在我们已经成功创建了字幕 clip 文件。\n合成视频 # 有了字幕片段 Clip，让我们将其与原视频合成，生成一个包含字幕的文件。\n代码如下所示：\nvideo = VideoFileClip(\u0026#34;input_video2.mp4\u0026#34;) video = CompositeVideoClip([video, subtitles_clip]) video.write_videofile(\u0026#34;output_with_subtitles.mp4\u0026#34;, audio_codec=\u0026#34;aac\u0026#34;) 执行完代码，就可以生成带有字幕的视频文件。\n效果如下所示：\n仔细观察左上角，可以看到字幕已经添加到视频上。\n字幕位置 # 我们检查生成的视频文件，发现生成字幕位于视频的左上角，这个效果并不是我想要的。\n如果想修改字幕在视频中的位置该如何做呢？\n字幕正常应该是位于视频下方居中的。我们可以通过 SubtitlesClip 的方法 set_position 调整位置。\n我们的目标是基于原视频，将字幕调整到位于视频底部上下 0.2 的位置，即坐标向下 0.8 的位置。\n实现代码如下所示：\nwidth, height = video.size pos = (\u0026#34;center\u0026#34;, height * 0.8) subtitles_clip = subtitles_clip.set_position(pos) 效果如下所示：\n字幕在视频底部。\n字幕样式 # 如果发现字体太小或者想修改字体或颜色，我们可以覆盖创建的 SubtitlesClip 的第三个三个参数 make_textclip。\n假设将默认字体大小改为 48，字体改为font 为 Arial，字体颜色改为 orange。\n实现代码如下所示：\ndef make_textclip(txt): return TextClip(txt, fontsize=48, font=\u0026#34;Arial\u0026#34;, color=\u0026#34;orange\u0026#34;) subtitles_clip = SubtitlesClip(subtitles, make_textclip) 如果发现生成字幕超出了原视频范围，可设置 TextClip 的参数 size 限制字幕宽度，设置参数 method 为 caption 实现自动换行。\n实现代码，如下所示，\ndef make_textclip(txt): return TextClip(txt, fontsize=48, font=\u0026#34;Arial\u0026#34;, color=\u0026#34;orange\u0026#34;, size=(width * 0.8, None), method=\u0026#34;caption\u0026#34;) 字幕宽度为原始视频宽度的 0.8 倍，如果大于设置宽度则自动换成。\n效果如下：\n到此，我们通过 openai-whisper 和 moviepy 制作自动化制作视频字幕的教程就完成了。\n总结 # 本博文主要介绍如何基于 openai-whisper 和 moviepy 实现自动化制作视频字幕。如果想了解字幕的更多样式，可查看 TextClip 还支持哪些参数。\n还有，有了这个能力。如想继续扩展，我们还可以通过调用三方翻译软件，实现给视频自动加载翻译字幕。\n感谢阅读，可持续关注我的博客。\n","date":"2024-01-05","externalUrl":null,"permalink":"/posts/2024-01-06-add-subtitles-using-moviepy-and-whipser/","section":"文章","summary":"OpenAI 因为发布了 ChatGPT 这款大模型产品，这两年是异常的火啊。ChatGPT 和 Sora 是自媒体上最常听到的 OpenAI 推出的两款产品。 除此以外，OpenAI 还有文生图的 Doll-E 模型，语音转文字的 Whisper，文字转语音的 OpenAI TTS。\n","title":"将 MoviePy 和 OpenAI Whisper 给视频添加字幕","type":"posts"},{"content":"前面的两篇教程，已经介绍了 MoviePy 的 VideoFileClip 和 AudioFileClip 的使用。本文将介绍 MoviePy 第三种基础 Clip - TextClip。\n前言 # 什么是 TextClip？TextClip，即是用于生成基于文本视频片段的类。\nTextClip 创建的包含文本的视频片段，一方面，我们可以将其作为单独的片段保存。另一方面，我们可以将其合成到其他视频中，这对于制作视频标题、说明，甚至字幕等内容非常有用。\n关于字幕制作，会有单独一篇博文，如介绍何基于 MoviePy 和 Whisper 实现自动给视频添加字幕。\n我们开始进入正题吧。\n快速上手 # 让我们直接创建一个 TextClip。\n示例代码，如下所示：\n# 创建一个包含文本的视频片段 text = \u0026#34;Hello MoviePy!\u0026#34; txt_clip = TextClip(text, color=\u0026#39;orange\u0026#39;, size=(100, 100)) # 为视频片段设置持续时间 video_duration = 10 # 设置视频持续时间为 10 秒 txt_clip = txt_clip.set_duration(video_duration).set_fps(1) # 保存生成的文本视频片段 txt_clip.write_videofile(\u0026#34;text_clip.mp4\u0026#34;) 这个例子展示了如何使用 TextClip 类创建一个文本视频片段。\n代码具体作用是，创建了一个持续时间为 10 秒的视频片段，其中包含 \u0026ldquo;Hello MoviePy!\u0026rdquo; 文本，颜色是 orange，字体是 AvantGrade-Book，视频大小 (100,100)。\n我们可以按需调整文本内容、字体，视频大小、颜色等，以及通过 set_duration 方法设置视频时长。\n注意一点，对于 TextClip，如果希望保存为视频，则必须指定 fps。我们接下来将要介绍的 ImageClip 也是如此。\n添加到视频 # 让我们把生成文本视频片段合成到现有的一个视频上。\n代码如下所示：\nvideo = VideoFileClip(\u0026#34;input_video.mp4\u0026#34;) video = CompositeVideoClips([video, txt_clip.set_duration(video.duration)]) video.write_videofile(\u0026#34;output_video_with_text.mp4\u0026#34;) 我们使用 CompositeVideoClips 类将 video 与 txt_clip 和合并成一个视频。\n通过 set_pos 方法，我们还可以指定文本居中显示，代码如下：\ntxt_clip.set_pos(\u0026#34;center\u0026#34;) 文本字体 # MoviePy 通过将系统字体内置，让我们可以直接在 TextClip 中使用内置字体名称设置字体，而无需指定字体的具体路径。\ntxt_clip = TextClip(text, color=\u0026#34;orange\u0026#34;, size=(100, 100), font=\u0026#34;AvantGarde-Book\u0026#34;) 如果查看 TextClip 支持的内置字体，可使用 TextClip.list(\u0026lsquo;font\u0026rsquo;) 列出所有支持的字体。\n示例代码，如下所示：\nbuiltin_fonts = txt_clip.list(\u0026#39;font\u0026#39;) # 打印内置字体列表 print(\u0026#34;Built-in Fonts in TextClip:\u0026#34;) print(builtin_fonts) 输出内容：\n[ \u0026#39;AvantGarde-Book\u0026#39;, \u0026#39;AvantGarde-BookOblique\u0026#39;, \u0026#39;AvantGarde-Demi\u0026#39;, ... \u0026#39;Yuppy-TC-Regular\u0026#39;, \u0026#39;Zapf-Dingbats\u0026#39;, \u0026#39;Zapfino\u0026#39; ] 例如，我们可以使用 Nerd Hack 字体生成包含 Icon 的文本内容。\n文本内容：\n实现代码：\ntxt_clip = TextClip( text, color=\u0026#39;white\u0026#39;, font=\u0026#34;MesloLGS-NF-Regular\u0026#34;, fontsize=36, size=(1920, 1080), ) txt_clip = txt_clip.set_duration(10).set_fps(1) txt_clip.write_videofile(\u0026#34;text_video.mp4\u0026#34;) 自定义字体路径 # 如果想在 TextClip 中使用特定字体，需要在系统中安装字体，或者代码中指定字体路径。\n假设，我现在有一个字体，可以直接指定字体自定义路径。\n示例代码如下：\n# 指定自定义字体的路径 custom_font_path = \u0026#34;/Users/jian.xue/Library/Fonts/MesloLGS NF Regular.ttf\u0026#34; # 创建一个包含文本的视频片段，并指定字体路径 text = \u0026#34;Hello MoviePy!\u0026#34; txt_clip = TextClip(text, fontsize=70, color=\u0026#39;white\u0026#39;, font=custom_font_path) TextClip 的底层 # TextClip 的底层使用的其实是 imagemagick，可直接通过 magick 命令列出它的所有字体。\n例如，查看支持所有字体，也可以通过如下命令。\nmagick -list font 输出如下：\n... Font: MesloLGS-NF-Bold family: MesloLGS NF glyphs: /Users/jian.xue/Library/Fonts/MesloLGS NF Bold.ttf Font: MesloLGS-NF-Bold-Italic family: MesloLGS NF glyphs: /Users/jian.xue/Library/Fonts/MesloLGS NF Bold Italic.ttf Font: MesloLGS-NF-Italic family: MesloLGS NF glyphs: /Users/jian.xue/Library/Fonts/MesloLGS NF Italic.ttf Font: MesloLGS-NF-Regular family: MesloLGS NF glyphs: /Users/jian.xue/Library/Fonts/MesloLGS NF Regular.ttf ... 总结 # 本文介绍了如何使用 TextClip 类创建包含特定文本的片段。我们可以调整文本内容、尺寸和视频持续时间等参数来生成不同的视频片段，创造出更多多样且独特的视频内容。\n如果觉得内容不错，可关注我的频道。\n","date":"2024-01-03","externalUrl":null,"permalink":"/posts/2024-01-04-moviepy-text-clip/","section":"文章","summary":"前面的两篇教程，已经介绍了 MoviePy 的 VideoFileClip 和 AudioFileClip 的使用。本文将介绍 MoviePy 第三种基础 Clip - TextClip。\n前言 # 什么是 TextClip？TextClip，即是用于生成基于文本视频片段的类。\n","title":"Python 视频剪辑库 - MoviePy 中 TextClip","type":"posts"},{"content":"上文介绍了如何安装 MoviePy 和利用 VideoFileClip 类读取视频文件，并进行一些简单的剪辑，如大小调整、旋转和片段剪辑等。\n本文将重点介绍 MoviePy 音频的一些操作。\n创建音频片段 AudioClip # MoviePy 中创建音频 Clip 的方法有两种，分别是从视频文件中提取音频，和基于音频文件创建。\n基于视频文件提取音频片段 # 我们现在有一个视频文件，名为 input_vidoe.mp4。\n直接通过如下代码提取音频文件，如下所示：\nfrom moviepy.editor import * video = VideoFileClip(\u0026#34;input_video.mp4\u0026#34;) audio = video.audio audio.write_audiofile(\u0026#34;ouput_audio.mp3\u0026#34;) 基于音频文件创建音频片段 # 通过 AudioFileClip 读取刚刚从视频中提取的音频文件即可。\n代码如下所示：\naudio = AudioFileClip(\u0026#34;output_audio.mp3\u0026#34;) 我们可以将这个视频短片加载到另外一个视频片段中。\n直接使用 TextClip 创建一个视频，我们通过 audio.duration 设置视频片段的播放时长。\nvideo = TextClip(\u0026#34;Hello MoviePy\u0026#34;, color=\u0026#34;orange\u0026#34;, size=(100, 100)) video = video.set_duration(audio.duration).set_fps(1) 让我们将音频加载到这个视频上。\n代码如下：\nvideo.audio = audio video.write_audio(\u0026#34;text_video.mp4\u0026#34;) 如果使用 mac 的 quicktimeplayer 打开，记得在写入的时候，加上参数 audio_codec=\u0026ldquo;aac\u0026rdquo;，即如下所示：\nvideo.write_audio(\u0026#34;text_video.mp4\u0026#34;, audio_codec=\u0026#34;aac\u0026#34;) 完成之后，即可打开视频，检查下效果。\n音频操作 # 介绍些 AudioClip 基础的操作，如音量的放大减小，音频片段裁剪等。\n音量控制 # 音频片段的音量控制，直接通过 AudioClip 的 volumex 方法实现。\n减低声音 0.5 倍的代码如下所示：\naudio = audio.volumex(0.5) 片段剪辑 # 现在，如果想剪辑音频中的一小段音频，和 VideoClip 一样，直接通过 subclip 实现即可。\n如剪辑从 5 到 15 秒的音频片段，代码如下所示：\naudio = audio.subclip(5, 15) 删除视频中的音频 # 我们再介绍一个小技巧，如果希望删除一个视频中的音频，我们可以在通过 VideoFileClip 读取视频文件时，指定参数 audio 为 False。\nvideo = VideoFileClip(\u0026#34;input_video\u0026#34;, audio=False) 或者亦可通过调用 VideoFileClip 的 without_audio 方法删除视频片段中的音频。\nvideo = video.without_audio() 是不是非常简单。\n总结 # 本文主要介绍了 MoviePy 中一些基础操作，更多关于 MoviePy 的教程，可订阅我的博客，感谢。\n","date":"2024-01-03","externalUrl":null,"permalink":"/posts/2024-01-03-moviepy-audio-file-clip/","section":"文章","summary":"上文介绍了如何安装 MoviePy 和利用 VideoFileClip 类读取视频文件，并进行一些简单的剪辑，如大小调整、旋转和片段剪辑等。\n本文将重点介绍 MoviePy 音频的一些操作。\n创建音频片段 AudioClip # MoviePy 中创建音频 Clip 的方法有两种，分别是从视频文件中提取音频，和基于音频文件创建。\n","title":"Python 视频剪辑库 - MoviePy 的音频操作","type":"posts"},{"content":"短视频时代，有许多用户友好的剪辑软件可用。不过，对于某些模式比较固定的视频，如果能自动化简化视频制作对于提高效率实际很重要的。\n这是本文将要讨论的内容。我们将利用一个名为 MoviePy 的 Python 库自动化您的视频制作过程。\n本博文将探索 MoviePy，展示它如何通过一些简单的 Python 代码简化视频剪辑过程。\n让我们开始吧。\n安装 # 要使用 MoviePy，首先需要进行安装。\n首先是安装 moviepy 本身，我们可以使用 Python 的包管理器 - pip 完成这一步。\n打开终端并执行如下命令：\npip install moviepy 必要的依赖项，如 ffmpeg，将会自动安装。\n如果想创建文本片段，还需要 imagemagick。可以使用 HomeBrew 进行安装。执行命令：\nbrew install imagemagick 另一个重要的依赖是 pygame，特别适用于预览正在创建的视频。这很重要，因为生成新视频可能需要一些时间。如果您希望在制作过程中预览您的工作，您将需要安装 pygame。\n当然！要安装 pygame，你可以在终端中执行:\npip install pygame 现在，MoviePy 的安装已经完成。让我们继续下一步 - 了解如何使用 MoviePy 编写代码。\n导入文件 # 在 MoviePy 中，要访问所有必要的函数和类，只需输入：\nfrom moviepy.editor import * 要使用 MoviePy 操作视频，要通过 VideoFileClip 导入视频文件，代码如下：\nvideo = VideoFileClip(\u0026#34;input_video.mp4\u0026#34;) 查看属性 # 有了 video 这个 VideoFIleClip 实例，我们可以通过它查看视频的一些属性。\nprint(f\u0026#34;video size: {video.size}\u0026#34;) # 视频的大小分辨率 print(f\u0026#34;video duration: {video.duration}\u0026#34;) # 视频的时间长度 print(f\u0026#34;video fps: {video.fps}\u0026#34;) # 视频的每秒多少帧 调整大小 # 让我们从简单调整视频大小开始。\n假设我现在有个视频，大小是 808x480，我的目标将其缩小 50%。只要一行代码轻松实现这个目标。\nvideo.resize(width=404, height=240) 可以看到我调用 video.resize 就将视频调整为 404x240 像素。\n调用 write_video_file 方法，可将其保存视频文件，如为 \u0026ldquo;resized_video.mp4\u0026rdquo;。\nvideo.write_videofile(\u0026#34;resized_video.mp4\u0026#34;) 完成后，检查结果是否符合期望。\n除了给出具体的长宽数值，如果是为了缩小 50%，还可以通过直接传递参数 0.5 实现。\nvideo = video.resize(0.5) 旋转 # 我们也可以使用 MoviePy 旋转视频。\n编写代码如下：\nvideo = video.rotate(180) 这段代码将使用简单的 video.rotate 方法将视频旋转 180 度。我们将旋转后的版本保存为 \u0026ldquo;rotated_video.mp4\u0026rdquo;。\nvideo.write_videofile(\u0026#34;rotated_video.mp4\u0026#34;) 打开后会发现视频已经颠倒了。\n片段剪辑 # MoviePy 当然支持剪辑能力。\n假设，我们想从长视频中剪辑出一个短视频非常简单，通过如下这段代码即可实现。\nvideo.subclip(5, 15).write_videofile(\u0026#34;subclip_5_15_video.mp4\u0026#34;) 我们使用 subclip 方法剪切从第 5 秒到第 15 秒的视频。我们将剪切版本保存为 \u0026ldquo;subclip_5_15_video.mp4\u0026rdquo;。\n当然，ffmpeg 可以使用以下命令来完成，但显然要复杂些。\nffmpeg -i input_video.mp4 -ss 00:00:05 -t 10 -c:v copy -c:a copy subclip_5_15_video.mp4 导出 GIF # 如果我们想从视频中创建一个 GIF，我们也可以做到！\n例如，我们的目标是提取第 5 秒到第 6 秒视频片段，将其调整缩小 50% 并旋转 180 度，将结果保存为 GIF 图片。\n代码如下所示：\nvideo.subclip(5, 6).resize(width=404).rotate(180).to_gif(\u0026#34;output_video.gif\u0026#34;) 经过 subclip resize 和 rotate 得到最终的 video，我们使用 to_gif 方法即可将其转换为名为 \u0026ldquo;output.gif\u0026rdquo; 的 GIF 文件。\n效果如下所示：\n视频合成 # MoviePy 还支持视频合成，如现在有两个视频文件，分别是经过 resize 和 rotate 的视频。我们可直接将这两个视频合并顺序播放。\nvideo1 = video.resize(0.5) video2 = video.rotate(90) video = concatenate_video_clips([video1, video2]) video.write_videofile(\u0026#34;output.mp4\u0026#34;) 我们这里只是简单演示，视频合并的详细使用方法，后面会有文章介绍。\n总结 # 本文简单展示了 MoviePy 视频处理的一些能力，没有详细展开介绍。如果你有兴趣进一步探索 MoviePy 的高级功能，请关注我的博客。\n最后，让我们来看看 MoviePy 官方文档中的一些复杂示例。\nCup Song 视频布局 马赛克追踪 3D 效果 下篇文章：MoviePy 中的音频操作\n","date":"2024-01-02","externalUrl":null,"permalink":"/posts/2024-01-03-moviepy-basic-usage/","section":"文章","summary":"短视频时代，有许多用户友好的剪辑软件可用。不过，对于某些模式比较固定的视频，如果能自动化简化视频制作对于提高效率实际很重要的。\n这是本文将要讨论的内容。我们将利用一个名为 MoviePy 的 Python 库自动化您的视频制作过程。\n","title":"Python 视频剪辑库 - MoviePy 的基础使用","type":"posts"},{"content":"本文是基于 Python 视频搬运系列教程的第 5 篇，介绍 mvideo 项目的初始化与资源下载的实现。\n前言 # 我们在上文介绍了如何基于 pytube 实现 YouTube 资源的下载，包括视频音频，甚至是字幕的下载。当然，字幕在本项目中暂时不在本项目的计划之中。\n本教程将会利用 pytube 这个 python 第三方包，我们将利用完成 YouTube 资源的下载。\n主流程 # 在编写代码之前，初始化这部分的基本流程，如下所示：\n我将按照这个流程，初始化项目，将资源下载到特定位置。\n如下是主流程的实现代码：\ndef init(): env = Environment() try: # 1. Set translator and urls to config env.set_translator(translator, translator_from_lang, translator_to_lang) # 2. extract YouTubes from playlist or urls videos = extract_videos(urls, playlist, playlist_start, playlist_end) if not videos: raise RuntimeError(\u0026#34;No videos found!\u0026#34;) env.set_urls([url for url in videos]) # 3. Download YouTubes for url, video in videos.items(): output_path = env.add_video( url, video.title, video.description, ) download_streams(env, video, output_path=output_path) except Exception: pass finally: # 4. Write config and data env.flush() 我们接下来的重点，只要完成 extract_videos 和 download_streams() 函数即可。\n查询可下载资源 # 实现函数 extract_videos，用于从传递的参数 playlist 或 urls 查询可用的 YouTube 列表，从而从中获取 stream 下载资源。\ndef extract_videos(urls, playlist, playlist_start, playlist_end): video_urls = None if playlist: playlist = Playlist(playlist) if playlist_start \u0026lt; playlist_end and playlist_end \u0026lt;= playlist.count: video_urls = playlist.video_urls[ playlist_start:playlist_end ] # pyright: ignore else: video_urls = playlist.video_urls else: video_urls = urls.split(\u0026#34;,\u0026#34;) videos = {} for url in video_urls: videos[url] = YouTube(url, on_progress_callback=on_progress) return videos playlist 优先于 urls，如果 playlist 存在，从 playlist 查询资源。\n下载音视频流 # 实现函数 download_streams 下载音视频资源，代码如下所示：\ndef download_streams(env: Environment, video: YouTube, output_path: str): print(\u0026#34;Downloading Video...\u0026#34;) stream = ( video.streams.filter(type=\u0026#34;video\u0026#34;, is_dash=True).order_by(\u0026#34;resolution\u0026#34;).last() ) stream.download( # pyright: ignore filename=env.video_filename, output_path=output_path ) print(\u0026#34;Downloading Audio...\u0026#34;) stream = video.streams.filter(type=\u0026#34;audio\u0026#34;).first() stream.download( # pyright: ignore filename=env.audio_filename, output_path=output_path ) 到这里，init 功能初步完成，我们下面测试功能是否可用吧。\n代码测试 # 假设现在我们地址为 www.youtube.com/watch?v=ceRYL271cao 的视频。\npython mvideo/main.py init --urls \u0026#34;www.youtube.com/watch?v=ceRYL271cao\u0026#34; 执行完成后，会将在当前位置创建一个目录用于存放资源，以及配置文件 config.toml 和数据文件 data.json。\n如下所示：\n$ ls be-a-tmux-king-with-tmuxifier-|-my-favorite-tmux-tool config.toml data.json 在 be-a-tmux-king-with-tmuxifier-|-my-favorite-tmux-tool 目录中是下载的音视频文件。\n$ ls audio.mp4 video.webm 最后 # 本教程实现了 init 初始化子命令，初始化目录和下载资源。下篇教程开始介绍如何使用 whipser 语音识别生成字幕。\n","date":"2023-12-09","externalUrl":null,"permalink":"/posts/2023-12-10-mvideo-init-function/","section":"文章","summary":"本文是基于 Python 视频搬运系列教程的第 5 篇，介绍 mvideo 项目的初始化与资源下载的实现。\n前言 # 我们在上文介绍了如何基于 pytube 实现 YouTube 资源的下载，包括视频音频，甚至是字幕的下载。当然，字幕在本项目中暂时不在本项目的计划之中。\n","title":"基于 Python 视频搬运 Part5 - 初始化与资源下载","type":"posts"},{"content":"本文是基于 Python 视频搬运的第三篇，也是一篇完整的 pytube 教程，介绍如何通过 pytube 下载 YouTube 的音视频等资源。\n概述 # pytube 是一款由 Python 实现，用于下载油管的第三方库，它的特点是无第三方的依赖，轻量，而且还有一个非常重要的点，它的接口灵活度高。\n而且，对于命令行，pytube 也提供了 pytube 命令也实现了通过命令行实现资源下载。当然，命令行下载视频的工具，更著名的 youtube-dl 和 you-get，它们比 pytube 出名，如果不是通过 Python 实现资源下载，它们或许是更好的选择。\n安装 # 如下命令安装 pytube：\npip install pytube 快速开始 # 我们通过一个案例演示如何使用 pytube 下载视频，视频地址：www.youtube.com/watch?v=ceRYL271cao。\nYoutTube 是 pytube 的核心类，它可用于获取某个 YouTube 视频的信息，包括基本属性内容等，如标题、音视频流，字幕等。\n视频属性 # from pytube import YouTube yt = YouTube(\u0026#34;https://www.youtube.com/watch?v=ceRYL271cao\u0026#34;) print(f\u0026#34;title: {yt.title}\\nthumbnail_url: {yt.thumbnail_url}\\nchannel_url: {yt.channel_url}\u0026#34;) 如上的代码获取了视频的标题、封面图和频道地址。\n输出结果：\ntitle: Be a tmux KING with Tmuxifier | My FAVORITE tmux tool thumbnail_url: https://i.ytimg.com/vi/ceRYL271cao/hq720.jpg channel_url: https://www.youtube.com/channel/UCo71RUe6DX4w-Vd47rFLXPg 更多属性：\n# dir 查看 YouTube 属性 dir(yt) 输出结果：\n[\u0026#39;__class__\u0026#39;, \u0026#39;__delattr__\u0026#39;, \u0026#39;__dict__\u0026#39;, \u0026#39;__dir__\u0026#39;, \u0026#39;__doc__\u0026#39;, \u0026#39;__eq__\u0026#39;, \u0026#39;__format__\u0026#39;, \u0026#39;__ge__\u0026#39;, \u0026#39;__getattribute__\u0026#39;, \u0026#39;__getstate__\u0026#39;, \u0026#39;__gt__\u0026#39;, \u0026#39;__hash__\u0026#39;, \u0026#39;__init__\u0026#39;, \u0026#39;__init_subclass__\u0026#39;, \u0026#39;__le__\u0026#39;, \u0026#39;__lt__\u0026#39;, \u0026#39;__module__\u0026#39;, \u0026#39;__ne__\u0026#39;, \u0026#39;__new__\u0026#39;, \u0026#39;__reduce__\u0026#39;, \u0026#39;__reduce_ex__\u0026#39;, \u0026#39;__repr__\u0026#39;, \u0026#39;__setattr__\u0026#39;, \u0026#39;__sizeof__\u0026#39;, \u0026#39;__str__\u0026#39;, \u0026#39;__subclasshook__\u0026#39;, \u0026#39;__weakref__\u0026#39;, \u0026#39;_age_restricted\u0026#39;, \u0026#39;_author\u0026#39;, \u0026#39;_embed_html\u0026#39;, \u0026#39;_fmt_streams\u0026#39;, \u0026#39;_initial_data\u0026#39;, \u0026#39;_js\u0026#39;, \u0026#39;_js_url\u0026#39;, \u0026#39;_metadata\u0026#39;, \u0026#39;_player_config_args\u0026#39;, \u0026#39;_publish_date\u0026#39;, \u0026#39;_title\u0026#39;, \u0026#39;_vid_info\u0026#39;, \u0026#39;_watch_html\u0026#39;, \u0026#39;age_restricted\u0026#39;, \u0026#39;allow_oauth_cache\u0026#39;, \u0026#39;author\u0026#39;, \u0026#39;bypass_age_gate\u0026#39;, \u0026#39;caption_tracks\u0026#39;, \u0026#39;captions\u0026#39;, \u0026#39;channel_id\u0026#39;, \u0026#39;channel_url\u0026#39;, \u0026#39;check_availability\u0026#39;, \u0026#39;description\u0026#39;, \u0026#39;embed_html\u0026#39;, \u0026#39;embed_url\u0026#39;, \u0026#39;fmt_streams\u0026#39;, \u0026#39;from_id\u0026#39;, \u0026#39;initial_data\u0026#39;, \u0026#39;js\u0026#39;, \u0026#39;js_url\u0026#39;, \u0026#39;keywords\u0026#39;, \u0026#39;length\u0026#39;, \u0026#39;metadata\u0026#39;, \u0026#39;publish_date\u0026#39;, \u0026#39;rating\u0026#39;, \u0026#39;register_on_complete_callback\u0026#39;, \u0026#39;register_on_progress_callback\u0026#39;, \u0026#39;stream_monostate\u0026#39;, \u0026#39;streaming_data\u0026#39;, \u0026#39;streams\u0026#39;, \u0026#39;thumbnail_url\u0026#39;, \u0026#39;title\u0026#39;, \u0026#39;use_oauth\u0026#39;, \u0026#39;vid_info\u0026#39;, \u0026#39;video_id\u0026#39;, \u0026#39;views\u0026#39;, \u0026#39;watch_html\u0026#39;, \u0026#39;watch_url\u0026#39;] 属性的具体含义，可查看 pytube 文档：YouTube Object\n音视频流 # pytube 与其他的油管视频下载工具不同，它不会默认选择资源下载，要我们手动 filter 选择要使用的资源。\n首先，查看 YouTube 中可用的音视频资源信息，通过 YoutTube 的 streams 属性获取。\n代码如下：\nprint(yt.streams) 输出结果：\n[\u0026lt;Stream: itag=\u0026#34;17\u0026#34; mime_type=\u0026#34;video/3gpp\u0026#34; res=\u0026#34;144p\u0026#34; fps=\u0026#34;7fps\u0026#34; vcodec=\u0026#34;mp4v.20.3\u0026#34; acodec=\u0026#34;mp4a.40.2\u0026#34; progressive=\u0026#34;True\u0026#34; type=\u0026#34;video\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;18\u0026#34; mime_type=\u0026#34;video/mp4\u0026#34; res=\u0026#34;360p\u0026#34; fps=\u0026#34;30fps\u0026#34; vcodec=\u0026#34;avc1.42001E\u0026#34; acodec=\u0026#34;mp4a.40.2\u0026#34; progressive=\u0026#34;True\u0026#34; type=\u0026#34;video\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;22\u0026#34; mime_type=\u0026#34;video/mp4\u0026#34; res=\u0026#34;720p\u0026#34; fps=\u0026#34;30fps\u0026#34; vcodec=\u0026#34;avc1.64001F\u0026#34; acodec=\u0026#34;mp4a.40.2\u0026#34; progressive=\u0026#34;True\u0026#34; type=\u0026#34;video\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;313\u0026#34; mime_type=\u0026#34;video/webm\u0026#34; res=\u0026#34;2160p\u0026#34; fps=\u0026#34;30fps\u0026#34; vcodec=\u0026#34;vp9\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;video\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;271\u0026#34; mime_type=\u0026#34;video/webm\u0026#34; res=\u0026#34;1440p\u0026#34; fps=\u0026#34;30fps\u0026#34; vcodec=\u0026#34;vp9\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;video\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;137\u0026#34; mime_type=\u0026#34;video/mp4\u0026#34; res=\u0026#34;1080p\u0026#34; fps=\u0026#34;30fps\u0026#34; vcodec=\u0026#34;avc1.640028\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;video\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;248\u0026#34; mime_type=\u0026#34;video/webm\u0026#34; res=\u0026#34;1080p\u0026#34; fps=\u0026#34;30fps\u0026#34; vcodec=\u0026#34;vp9\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;video\u0026#34;\u0026gt;, ... \u0026lt;Stream: itag=\u0026#34;139\u0026#34; mime_type=\u0026#34;audio/mp4\u0026#34; abr=\u0026#34;48kbps\u0026#34; acodec=\u0026#34;mp4a.40.5\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;audio\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;140\u0026#34; mime_type=\u0026#34;audio/mp4\u0026#34; abr=\u0026#34;128kbps\u0026#34; acodec=\u0026#34;mp4a.40.2\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;audio\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;249\u0026#34; mime_type=\u0026#34;audio/webm\u0026#34; abr=\u0026#34;50kbps\u0026#34; acodec=\u0026#34;opus\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;audio\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;250\u0026#34; mime_type=\u0026#34;audio/webm\u0026#34; abr=\u0026#34;70kbps\u0026#34; acodec=\u0026#34;opus\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;audio\u0026#34;\u0026gt;, \u0026lt;Stream: itag=\u0026#34;251\u0026#34; mime_type=\u0026#34;audio/webm\u0026#34; abr=\u0026#34;160kbps\u0026#34; acodec=\u0026#34;opus\u0026#34; progressive=\u0026#34;False\u0026#34; type=\u0026#34;audio\u0026#34;\u0026gt;] pytube 支持 filter 过滤资源，如只要视频资源，选择分辨率最高的视频。\n代码如下：\nstream = yt.streams.filter(type=\u0026#34;video\u0026#34;).order_by(\u0026#34;resolution\u0026#34;).last() 首先，通过 type=\u0026quot;video\u0026quot; 过滤视频资源，接着按 resolution，即分辨率，升序排序视频，再最后一个视频。\n视频下载 # 一旦获取到我们的目标 stream，就可以调用 download 方法，即可下载。\nstream.download() 默认情况，download 下载位置为当前目录，并以 YouTube 视频标题作为文件名。如希望修改这个默认行为，可通过 filename 和 output_path 实现。filename 执行下载文件名，output_path 执行文件下载路径。\n音视频分离 # 现在，我们得到了一个视频文件，但这极有可能一个无声视频。\npytube 官方文档有如下这么一段话：\nYou may notice that some streams listed have both a video codec and audio codec, while others have just video or just audio, this is a result of YouTube supporting a streaming technique called Dynamic Adaptive Streaming over HTTP (DASH). In the context of pytube, the implications are for the highest quality streams; you now need to download both the audio and video tracks and then post-process them with software like FFmpeg to merge them. The legacy streams that contain the audio and video in a single file (referred to as “progressive download”) are still available, but only for resolutions 720p and below. 简单来说，就是部分 stream 同时包含音视频，而部分 stream 是音视频分离的。而同时包含视频的流使用的是以前的 Progressive Adaptive 技术，音视频分离使用的是 Dynamic Adaptive Streaming over HTTP，即 DASH 技术。\n关键是，Progressive Adaptive 的视频最高分辨率是 720p，更高分辨率则必须是用 Dash 技术的流，分别下载音频和视频，再通过类似 FFmpeg 的工具将两者合并。\n我们现在就不能只下载视频资源了。\n修改后的代码，如下所示：\ndash_streams = yt.streams.filter(is_dash=True) video_stream = dash_streams.filter(type=\u0026#34;video\u0026#34;).order_by(\u0026#34;resolution\u0026#34;).last() audio_stream = dash_streams.filter(type=\u0026#34;audio\u0026#34;).first() video_stream.download() audio_stream.download() 到此，我们成功获取需要的音视频资源了。\n下载进度 # 默认的配置，pytube 下载视频不展示下载进度，可通过在创建 YouTube 类时指定回调.\n代码如下所示：\nfrom pytube import YouTube from pytube.cli import on_progress watch_url = \u0026#34;https://www.youtube.com/watch?v=ceRYL271cao\u0026#34; yt = YouTube(watch_url, on_progress_call=on_progress) 除了 on_progress_callback，还可指定 on_complete_callback，用于指定下载完成的回调。\n频道与播放列表 # 如何更好地使用 pytube 管理与下载资源呢？\nYouTube 核心有 4 个 Object 类，即 Channel、PlayList、YoutTube 和 Stream。\n前面已经演示过 YouTube 和 Stream 类的基本使用。要用好 pytube，重点是了解这 4 个类间的关系。\n简言之，Channel 中包含 PlayList 和 YouTube。而 PlayList 中包含 YouTube。YouTube 中包含 Stream。即Channel 和 Playlist 是 YouTube 的容器。\n如果实现频道视频检测的功能，可利用 Channel 和 PlayList 这两个类，定时检测 Channel 和 PlayList 中的 video_urls。\n创建 Channel 的代码：\nfrom pytube import Channel ch = Channel(\u0026#34;https://www.youtube.com/channel/UCo71RUe6DX4w-Vd47rFLXPg\u0026#34;) 频道的创建要传入 channel 地址，而这个地址，因为 YouTube 的改版，网页端中的 channel 地址不可用，而要通过 YoutTube 的 channel_url 属性获取。\n示例代码如下：\nyt = YoutTube(\u0026#34;https://www.youtube.com/watch?v=ceRYL271cao\u0026#34;) print(yt.channel_url) 输出结果：\nhttps://www.youtube.com/channel/UCo71RUe6DX4w-Vd47rFLXPg 特别说明，因为 youtube 的改版，pytube 中有些接口已经不可用了，要修复。如，Channel 中的一些功能不可用，这篇文章有介绍修复方案：解决近期Pytube的Channel Video列表会空的问题。\n通过 Channel 获取所有的 YouTube 或视频地址列表：\nprint(ch.videos) print(ch.video_urls) 播放列表 playlist 的创建，可通过播放 playlist 地址或视频地址中包含 playlist 的参数。\n代码如下所示：\nfrom pytube import Playlist p = Playlist(\u0026#34;https://www.youtube.com/playlist?list=PLsz00TDipIfdGZRWNvOdeZfbka9HfpYBg\u0026#34;) # p = Playlist(\u0026#34;https://www.youtube.com/watch?v=capyZ2D9Yz0\u0026amp;list=PLsz00TDipIfdGZRWNvOdeZfbka9HfpYBg\u0026amp;ab_channel=typecraft\u0026#34;) 通过 playlist 获取所有 YoutTube 或地址列表：\nprint(p.videos) print(p.video_urls) 搜索能力 # pytube 还提供了搜索油管的能力，或许一些场景会有用吧。\n使用代码如下：\nfrom pytube import Search results = Search(\u0026#34;zsh\u0026#34;).results print(results) 其中返回的 results 就是 YouTube 的实例数组。如有需要，从中选择需要的 YoutTube 兑现个，在其上执行 streams 下载即可。\n命令行工具 - pytube # pytube 除了以 python 库的形式提供视频下载能力，还提供了一个 pytube 的命令行工具。\n以一些示例说明 pytube 命令的使用吧，如下所示：\n下载包含音频且最高分辨率视频：\npytube https://www.youtube.com/watch?v=ceRYL271cao 查看所有可用的流：\npytube https://www.youtube.com/watch?v=ceRYL271cao --list 下载指定的流：\npytube https://www.youtube.com/watch?v=ceRYL271cao -itag=22 查看可用字幕：\npytube https://www.youtube.com/watch?v=ceRYL271cao -list-caption 下载执行字幕：\npytube https://www.youtube.com/watch?v=ceRYL271cao -c en 下载音频：\npytube https://www.youtube.com/watch?v=ceRYL271cao -a 最后 # 本文介绍了如何使用 pytube 下载油管资源，如果大家希望通过 python 管理下载视频，希望它能有所有帮助。\n我的博文：基于 Python 视频搬运 Part2 - pytube 下载 YouTube 资源。\n","date":"2023-12-09","externalUrl":null,"permalink":"/posts/2023-12-09-pytube-tutorial/","section":"文章","summary":"本文是基于 Python 视频搬运的第三篇，也是一篇完整的 pytube 教程，介绍如何通过 pytube 下载 YouTube 的音视频等资源。\n概述 # pytube 是一款由 Python 实现，用于下载油管的第三方库，它的特点是无第三方的依赖，轻量，而且还有一个非常重要的点，它的接口灵活度高。\n","title":"基于 Python 视频搬运 Part4 - pytube 下载 YouTube 资源","type":"posts"},{"content":"本文介绍 mvideo 项目如何管理配置和视频搬运过程中的数据。\n前言 # 在这个视频的处理过程中，我们会保存一些过程中的配置或者数据，如这是否是一个翻译类的项目，要处理 url 地址等等。\n此外，处理过程中的数据，如视频的标题、描述等等，要需要保存下来，便于后续使用，而不是每次都要重复下载。\n还有，为增强核心代码的可读性，提高代码的封装性和后续的扩展性，将如资源的下载目录，每个资源的路径统一管理，封装成一些特定的方法，将更易于使用。\n资源目录 # 每个资源会下载当前目录下的以视频标题作为名称的子目录，其中的结构如下所示：\n./cover.png ./how-to-learn-neovim-1/ # 视频标题小写同时空格替换为 - - video.webm # 下载的视频文件 - audio.mp4 # 下载的音频文件 - audio.wav # 从下载视频中抽取的 wav 文件，用于字幕识别 - audio.srt # 识别的字幕文件 - audio_translate.srt # 字幕的翻译文件 - final.mp4 # 最终成品视频 如果每个功能函数都管理这些文件名，将着实很恼人，而且说不定还有写错。\n管理类 Environment # 我将定义一个类，名为 Environment，单独处理这类信息。\nclass Environment: def __init__(self): self._project_path = os.path.abspath(\u0026#34;.\u0026#34;) self._config_path = os.path.join(self._project_path, \u0026#34;config.toml\u0026#34;) self._config = ( toml.load(open(self._config_path)) if os.path.exists(self._config_path) else {} ) self._urls = self._config.get(\u0026#34;urls\u0026#34;, []) self._translator = self._config.get(\u0026#34;translator\u0026#34;, {}) self._data_path = os.path.join(self._project_path, \u0026#34;data.json\u0026#34;) self._data = ( json.load(open(self._data_path)) if os.path.exists(self._data_path) else {} ) self._video_filename = \u0026#34;video.webm\u0026#34; self._audio_filename = \u0026#34;audio.mp4\u0026#34; self._audio_wav_filename = \u0026#34;audio.wav\u0026#34; self._audio_srt_filename = \u0026#34;audio.srt\u0026#34; self._trans_srt_filename = \u0026#34;audio_translate.srt\u0026#34; self._cover_image_filepath = \u0026#34;cover.png\u0026#34; self._final_filename = \u0026#34;final.mp4\u0026#34; self._videos = self._data.get(\u0026#34;videos\u0026#34;, {}) 初始化阶段，首先会将必要的信息准备好，从配置文件 config.toml 读取配置，从 data.json 中读取数据。\n还有，搬运过程中的一些乱七八糟的文件，如下载音视频文件名称，产生的字幕文件等等，也都定义在了这里。\n辅助函数 # Environment 还将一些这些属性通过辅助函数的形式提供出来。\n如下是一些配置获取的辅助方法：\n@property def project_path(self): return self._project_path def project_subpath(self, subpath): return os.path.join(self._project_path, subpath) def set_urls(self, urls): self._urls = urls def set_translator(self, translator, from_lang, to_lang): self._translator = { \u0026#34;translator\u0026#34;: translator, \u0026#34;from_lang\u0026#34;: from_lang, \u0026#34;to_lang\u0026#34;: to_lang, } def translator(self, translator): if translator: return translator return self._translator.get(\u0026#34;transaltor\u0026#34;) def translator_from_lang(self, from_lang): if from_lang: return from_lang return self._translator.get(\u0026#34;from_lang\u0026#34;) def translator_to_lang(self, to_lang): if to_lang: return to_lang return self._translator.get(\u0026#34;to_lang\u0026#34;) 一些视频相关的辅助方法：\n@property def videos(self): return self._videos def add_video(self, url, title, description): output_path = title.replace(\u0026#34; \u0026#34;, \u0026#34;-\u0026#34;) video_path = os.path.join(self._project_path, output_path) self._videos[output_path] = { \u0026#34;url\u0026#34;: url, \u0026#34;title\u0026#34;: title, \u0026#34;path\u0026#34;: video_path, \u0026#34;description\u0026#34;: description, \u0026#34;video_filepath\u0026#34;: os.path.join(video_path, \u0026#34;video.wepm\u0026#34;), \u0026#34;audio_filepath\u0026#34;: os.path.join(video_path, \u0026#34;audio.mp4\u0026#34;), \u0026#34;audio_wav_filepath\u0026#34;: os.path.join(video_path, \u0026#34;audio.wav\u0026#34;), \u0026#34;audio_srt_filepath\u0026#34;: os.path.join(video_path, self._audio_srt_filename), \u0026#34;trans_srt_filepath\u0026#34;: os.path.join(video_path, self._trans_srt_filename), \u0026#34;cover_filepath\u0026#34;: os.path.join(video_path, self._cover_image_filepath), \u0026#34;final_filepath\u0026#34;: os.path.join(video_path, self._final_filename), } return output_path @property def video_filename(self): return \u0026#34;video.webm\u0026#34; @property def audio_filename(self): return \u0026#34;audio.mp4\u0026#34; def video_filepath(self, output_path): return self._videos[output_path].get(\u0026#34;video_filepath\u0026#34;) def audio_filepath(self, output_path): return self._videos[output_path].get(\u0026#34;audio_filepath\u0026#34;) def audio_wav_filepath(self, output_path): return self._videos[output_path].get(\u0026#34;video_filepath\u0026#34;) def audio_srt_filepath(self, output_path): return self._videos[output_path].get(\u0026#34;audio_srt_filepath\u0026#34;) def trans_srt_filepath(self, output_path): return self._videos[output_path].get(\u0026#34;trans_srt_filepath\u0026#34;) def cover_filepath(self, output_path): return self._videos[output_path].get(\u0026#34;cover_image_filepath\u0026#34;) def final_filepath(self, output_path): return self._videos[output_path].get(\u0026#34;final_filepath\u0026#34;) 本质上，其实就是提供了一些 get 和 set 方法，写核心的代码更舒适一些。\n如想获取成品视频文件路径，通过如下代码即可实现：\nenv = Environment() final_filepath = env.final_filepath(\u0026#34;how-to-learn-neovim-part1/\u0026#34;) print(final_filepath) 更新写入 # Environment 提供了很多 get set 方法，在这个视频处理过程中，可能会改变配置和数据文件，这时候，需要执行 env.flush 才能将内容写入到 config.toml 和 data.json 文件中。\n为了防止异常中断写入，通过类似 try except 的方式保证写入成功。\nenv = Environment() try: env.set_urls(urls) ... except Exception: pass finally: env.flush() 最后 # 本文介绍了一个辅助类，用于统一管理全局的一些配置和过程中产生的数据，同时通过辅助函数提高代码的封装，便于后续扩展。\n","date":"2023-12-08","externalUrl":null,"permalink":"/posts/2023-12-08-mvideo-environment-to-manage-config-and-data/","section":"文章","summary":"本文介绍 mvideo 项目如何管理配置和视频搬运过程中的数据。\n前言 # 在这个视频的处理过程中，我们会保存一些过程中的配置或者数据，如这是否是一个翻译类的项目，要处理 url 地址等等。\n","title":"基于 Python 视频搬运 Part3 - 配置和数据的统一管理","type":"posts"},{"content":"本文如何搭建一套免费的博客，实现可基于 markdown 撰写博客，使用免费环境部署，以及添加评论、图床和统计等附加能力。\n这是一整套的搭建免费博客的可用方案。\n前言概述 # 从几年前开始写博客，当时把博客建在一台云主机上。另外，还单独买了一个域名 - poloxue.com。大概猜测，最初的大家都是有这种方式搭建自己博客。\n这种方式有两点不令我满意：\n首先是费用问题，虽说不是很贵，但一年也要 200-300，如果只是偶尔写写，不如发到一些内容平台。国内的博客平台还是挺多的，如 csnd、博客园、掘金等，都可利用。\n再者是维护问题，单独购买一台云服务器，即使是偶尔可能的宕机，也是要维护查原因。我的博客在 2018 年搭建，后来不知道什么原因，突然宕机后，当时我工作太忙，停了好几年也没重开；\n新的方案 # 互联网发展到今天，对博客这类常规需求，我们有免费的云服务使用，而且基本不会宕机，如果有大流量，也不用担心流量扩容的问题。\n我的新方案就是，利用 Hugo + GitHub Page 免费搭建我的博客，实现一套免费的解决方案。其他组件如评论、图床和统计也都有免费的服务可用。\nHugo # 第一个要解决的问题是，如何如何编写博客？\n我们可通过 markdown 编写文章，然后利用静态网页生成器将 markdown 文档转化为 html 文档。如此即可部署到静态网页托管的服务。\n将 markdown 转化 html，我推荐使用 Hugo。\n什么是 Hugo？Hugo 是 Go 编写的静态网页生成器，阅读它的 官方文档。\nHugo 利用的是 markdown 语法编写的文章渲染成 HTML 网页，它除了提供了大量的模版样式，还支持语法函数，实现站点的定制化。\nHugo 对于 blog 个人站点类的需求是特别适合，快速上手，简单易用。\nGitHub Page # 有了网页内容，下面就是要找可提供免费部署的服务了。我推荐使用 GitHub Page。\n什么是 GitHub Page？\nGitHub Page，是GitHub 提供的免费云服务，它是以代码仓库形式托管 HTML 静态页面的服务。\nGitHub Page 的优势在于，第一点是免费，无需购买机器。再者，使用云服务，能减少日常维护的时间，省心省力，还有一点，无需担心扩容问题。\n其他组件 # 一个 blog，即使是个人博客，也不是只要展示内容即可的，其他如评论、图床、统计等也是必不可少的。好在，这些组件同样也皆有使用免费的方案。\n评论 utterances # 评论功能可使用 utterances 实现，一个轻量级的评论组件。它是基于 GitHub issue 实现，数据全部存储于 GitHub 中。\n评论区的效果如下所示：\n和其他一些组件相比，这也是一个纯净版的评论组件，干净整洁无广告。它的 GitHub 仓库地址：utterances\n图床 jsdelivr CDN # 对于博客而言，图片是必不可少的一部分，没有图片的博客，固然会少了一些趣味。我的方案中，图床是通过 jsdelivr CDN 加速访问 GitHub 中存储的图片。\njsdelivr 是一款用于开源项目的免费 CDN，它支持加速入 npm、ESM、GitHub、WordPress。\n它的使用方式也非常简单。\n一个例子，假设以 GitHub 仓库 poloxue/images 下 main 分支的名为 tag-zsh.png 的图片为例。如想通过 jsdelivr CDN 访问，只要以如下的形式访问即可。\nhttps://cdn.jsdelivr.net/gh/poloxue/images/@main/tag-zsh.png 还是非常简单的。\n本质上这使用的还是 GitHub 作为存储方案，这是势必要将 GitHub 的羊毛耗光啊。\n统计能力 - goatcounter # 博客统计分析能力，我选择的是 goatcounter 这个免费的项目。\ngoatcounter 是一个开源项目，它提供了两种方式，分别是使用它的托管服务或使用它的源码自建。\n但它对个人用户，特别是开发人员还是非常易于使用。而且，它托管服务是免费的。\n如下是它的界面效果：\n相对于 Google Analytic，goatcounter 提供的能力没有那么强大，除了基本的访问数量统计，其他一些基本能力，如设备、语言、浏览器、操作系统的分析也都是支持的。\n如下所示： 最后 # 本文介绍了一套搭建免费博客的可用组件方案，不是介绍详细的安装教程。希望在 2023 年，给于有兴趣搭建自己博客的朋友一点帮助吧。\n","date":"2023-12-06","externalUrl":null,"permalink":"/posts/2023-12-06-create-your-own-free-blog/","section":"文章","summary":"本文如何搭建一套免费的博客，实现可基于 markdown 撰写博客，使用免费环境部署，以及添加评论、图床和统计等附加能力。\n这是一整套的搭建免费博客的可用方案。\n前言概述 # 从几年前开始写博客，当时把博客建在一台云主机上。另外，还单独买了一个域名 - poloxue.com。大概猜测，最初的大家都是有这种方式搭建自己博客。\n","title":"介绍一个搭建免费博客的实现方案","type":"posts"},{"content":"本文介绍基于 Python 视频搬运项目的代码布局。\n前言概述 # 项目的代码布局要从需求出发，一方面是既要满足当前的项目功能，也能保证一定的结构性便于后续扩展代码。\n这个工具本质是一个命令行工具，我在 先导篇 中介绍了该项目的目标。\n我们用了大量的子命令，我将用 click 这个 python 包解耦分离这个命令的功能。关于 click 的介绍，可查看其 官方文档。\n命令布局 # 本项目核心子命令一共 4 个，分别是 init、trascribe、make 和 publish，统一使用 click 包的 click.group 包裹为子命令。\n与之相应的一些核心文件的分布情况，如下所示：\n- mvideo/__init__.py - mvideo/main.py - mvideo/cmds/__init__.py - mvideo/cmds/init.py - mvideo/cmds/transcribe.py - mvideo/cmds/make.py - mvideo/cmds/publish.py cmds 目录下是所有我们要实现的子命令。\nmain 文件 # main.py 中是命令的入口文件，用于定义 main 命令。\n代码如下所示：\nimport click import mvideo.cmds as cmds click.group() def cli(): pass def main(): cli.add_command(cmds.transcribe) cli.add_command(cmds.init) cli.add_command(cmds.make) cli.add_command(cmds.publish) cli() if __name__ == \u0026#34;__main__\u0026#34; main() 我们将 init, transcribe, make 和 publish 挂在了 cli 命令组下面。\ninit 初始化下载资源 # init 初始化下载资源，代码如下：\nimport click @click.command(\u0026#34;init\u0026#34;) @click.option(\u0026#34;--urls\u0026#34;, type=click.STRING, help=\u0026#34;The list of video URL\u0026#34;) @click.option(\u0026#34;--playlist\u0026#34;, type=click.STRING, help=\u0026#34;Playlist URL\u0026#34;) @click.option(\u0026#34;--playlist-start\u0026#34;, type=click.INT, help=\u0026#34;Playlist start index\u0026#34;) @click.option(\u0026#34;--playlist-end\u0026#34;, type=click.INT, help=\u0026#34;Playlist end index\u0026#34;) @click.option(\u0026#34;--translator\u0026#34;, type=click.STRING, help=\u0026#34;Translator\u0026#34;) @click.option(\u0026#34;--translator-from-lang\u0026#34;, type=click.STRING, help=\u0026#34;Translator from lang\u0026#34;) @click.option(\u0026#34;--translator-to-lang\u0026#34;, type=click.STRING, help=\u0026#34;Translator to lang\u0026#34;) def init( urls, playlist, playlist_start, playlist_end, translator, translator_from_lang, translator_to_lang, ): pass init 参数说明：\n--urls，指定视频地址列表，指定单个或多个视频地址，多视频会自动合成一个视频； --playlist，指定视频播放列表地址，优先于 urls； --playlist-start/\u0026ndash;playlist-end，指定下载播放列表范围，从哪个开始下载到哪个结束； --translator，指定翻译器，默认为 none，表示无，通过 ls-translators 列出所有翻译器； --translator-from-lang，用于指定翻译的原始语言，默认为 en； --translator-to-lang，用于指定翻译的目标语言，默认为 zh； transcribe 音频转录与翻译 # transcribe 代码如下所示：\n@click.command(\u0026#34;transcribe\u0026#34;) @click.option(\u0026#34;--whisper-mode\u0026#34;, type=click.STRING, help=\u0026#34;whisper model\u0026#34;) @click.option(\u0026#34;--translator\u0026#34;, type=click.STRING, help=\u0026#34;Translator\u0026#34;) @click.option(\u0026#34;--translator-from-lang\u0026#34;, type=click.STRING, help=\u0026#34;Translator from lang\u0026#34;) @click.option(\u0026#34;--translator-to-lang\u0026#34;, type=click.STRING, help=\u0026#34;Translator to lang\u0026#34;) def transcribe(whisper_mode, translator, translator_from_lang, translator_to_lang): pass transcribe 参数说明：\n--whipser-mode，whisper 模型名，默认 base，可选 tiny, base, small, medium, large。 --translator，指定翻译器，默认值取决于项目配置； --translator-from-lang，用于指定翻译的原始语言，默认为 en； --translator-to-lang，用于指定翻译的目标语言，默认为 zh； make 合成制作视频 # make 制作视频，代码如下：\n@click.command(\u0026#34;make\u0026#34;) @click.option(\u0026#34;--cover-text\u0026#34;, type=click.STRING, help=\u0026#34;Covert text\u0026#34;) @click.option(\u0026#34;--declaim-text\u0026#34;, type=click.STRING, help=\u0026#34;Declaim text\u0026#34;) @click.option(\u0026#34;--end-text\u0026#34;, type=click.STRING, help=\u0026#34;End text\u0026#34;) @click.option(\u0026#34;--without-chapter\u0026#34;, is_flag=True, help=\u0026#34;Without chapter\u0026#34;) @click.option(\u0026#34;--smart\u0026#34;, is_flag=True, help=\u0026#34;If final video exists, dont override\u0026#34;) @click.option( \u0026#34;--subtitle-mode\u0026#34;, type=click.STRING, help=\u0026#34;Subtitle, options: all, origin, translate, none\u0026#34;, ) def make(cover_text, declaim_text, end_text, without_chapter, smart, subtitle_mode): pass make 参数说明；\n--cover-text，封面标题，视频开头，封面图存放于 output-path 目录下，如未指定则不制作； --declaim-text，声明文字，声明文本，用于生成视频声明，未指定则无声明； --end-text，结尾文本，用于感谢观众或提醒点赞关注等，未指定则无结尾提醒； --without-chapter，禁用默认的章节提示； --smart，存在成品视频，跳过不合成，否则合成视频； publish 发布视频 # import click @click.command(\u0026#34;publish\u0026#34;) @click.option( \u0026#34;--platform\u0026#34;, type=click.STRING, help=\u0026#34;Platform where you want to publish\u0026#34; ) @click.option(\u0026#34;--title\u0026#34;, type=click.STRING, help=\u0026#34;Title of video\u0026#34;) @click.option(\u0026#34;--source-url\u0026#34;, type=click.STRING, help=\u0026#34;Origin url of this video\u0026#34;) @click.option(\u0026#34;--keywords\u0026#34;, type=click.STRING, help=\u0026#34;Keywords of this video\u0026#34;) def publish(platform, title, source_url, keywords): pass --platform，指定视频的发布平台，默认值为 bilibili 平台，当前只支持 bilibili； --title，指定视频发布标题，默认使用第一个视频标题，启用翻译的话，使用翻译版本； --source-url，指定搬运来源，默认值，urls 的第一个视频地址或 playlist 即 playlist 地址； --keywords，指定视频标签，默认使用 youtube 视频的第一个关键字； 最后 # 本文介绍了 mvideo 视频搬运项目的代码布局结构，以及主要子命令的入口代码，接下来的重点就是结合如何实现每个子命令的功能。\n","date":"2023-12-02","externalUrl":null,"permalink":"/posts/2023-12-08-move-video-project-layout/","section":"文章","summary":"本文介绍基于 Python 视频搬运项目的代码布局。\n前言概述 # 项目的代码布局要从需求出发，一方面是既要满足当前的项目功能，也能保证一定的结构性便于后续扩展代码。\n","title":"基于 Python 视频搬运 Part2 - 代码布局","type":"posts"},{"content":" 本文介绍如何基于 Python 实现的从 YouTube 自动化搬运视频到国内平台的命令行工具，计划命名为 mvideo，即 move video 的意思。\n这个工具我已经有了一个版本，但我想把它作为一个案例，把它做成一个系统化的工具，便于后续扩展，故而，就借着视频平台以视频的形式一步一步实现这第一个版本。\n前言 # 话说，我为什么会想开发这样一款视频搬运工具呢？\n出国的几年，在 Youtube 发现不少免费的教程视频。或许是因为 Youtube 广告机制收入不菲，与程序员有关的免费教程和频道非常之多。我就想着搬运一些视频分享到国内的小伙伴。\n搬运的话，手动或自动化搬运皆可。但为搬运更多视频，能自动化肯定是最好的，而且技术视频搬运这事情也不挣啥钱，纯纯的慈善事业，还是要更多地专注于其他事情。\n我对这个工具的期望是能支持从下载、字幕识别、翻译、字幕制作、封面制作，甚至是多视频合成，或者大视频拆分，最终自动上传。\n另外，由于我希望搬运的视频是我看过的，所以我没有做自动监听频道直接搬运的能力。后期可以考虑，对于一些优质频道，无脑搬运也不是不行。\n我从网上搜罗了不少资料，花了一星期的时间，最终写出了这个小工具。除了大视频的拆分，其他基本都已经支持了。还有，自动发布当前只支持 bilibili。\n我在 B 站顺便还开通了一个频道 - Youtube技术视频，用于我的日常视频搬运。\n方案 # 资源下载，Youtube 资源的下载使用的 pytube 实现，一款轻量的用于下载 Youtube 资源的 Python 包。\n字幕制作，这其中主要涉及两点内容，分别是语音的文字识别和翻译。\n字幕识别，使用 openai 开发开源的语音识别系统 whisper，它支持多种模型，断句不错，而且精度比视频平台默认的文字转语音准确率看起来更好。\n字幕翻译，使用的是 python 的翻译库 translators ，它实现市面上大部分翻译渠道的对接，如 baidu, qqFanyi, google, bing 等都是支持的。如果想要高品质的翻译，则是需要花钱的，我当前只集成了 deepl 和 qqFanyi 两个付费的翻译器。\n视频合成，包括封面和其他一些图片制作，字幕、音视频的合成，使用的是 python 的 moviepy 库实现，它基于如 ffmpeg 和其他一些图片、视频处理库的一个易用使用 Python 音视频剪辑库。\n自动发布，通过 selenium 实现，当前支持持 B 站，不过，很多视频平台都有提供开放平台 OpenAPI，可以通过接口管理视频，暂时还没去暂时，到最后也可以考虑下。\n项目需求 # 接下来，说明下这个项目的目标。毕竟，做项目要有需求的不是。\n为了使项目的整体结构清晰，我将这个搬运项目不同功能拆解到了不同的命令上。效果上，有点以类似项目管理的方式。\n核心四个基本步骤，如下所示：\ninit， 实现项目初始化和音视频素材下载；transcribe，用于生成原始和翻译字幕；make，用于合成成品视频； publish，用于发布视频到特定视频平台。\n创建项目 # 新建视频搬运项目\nmkdir project_directory cd project_directory mvideo init --urls \u0026#34;https://youtube.com/watch?v=xxxx\u0026#34; 亦或是\nmvideo init --playlist \u0026#34;https://www.youtube.com/playlist?list=xxxx\u0026#34; --playlist-start 0 --playlist-end 3 该命令的作用是项目初始化，通过指定 url 下载音视频素材文件到项目下的视频标题命名的目录中。\n项目相关的资源文件，如下载、制作过程中和最终成品的文件都会存放在项目目录下。如想要纠正机翻中的错误字幕，直接编辑对应的文件即可。\n参数说明：\n--urls，指定视频地址列表，指定单个或多个视频地址，多视频会自动合成一个视频； --playlist，指定视频播放列表地址，优先于 urls； --playlist-start/\u0026ndash;playlist-end，指定下载播放列表范围，从哪个开始下载到哪个结束； 对于搬运翻译类的项目，可通过如下命令指定：\nmvideo init --translator \u0026#39;bing\u0026#39; --translator-from-lang \u0026#39;en\u0026#39; --translator-to-lang \u0026#39;zh\u0026#39; --translator，指定翻译器，默认为 none，表示无，通过 ls-translators 列出所有翻译器； --translator-from-lang，用于指定翻译的原始语言，默认为 en； --translator-to-lang，用于指定翻译的目标语言，默认为 zh； 字幕制作 # 通过如下命令生成视频原始字幕：\nmvideo transcribe --whisper-mode base --whipser-mode，whisper 模型名，默认 base，可选 tiny, base, small, medium, large。 注：关于 whisper 的更多支持，请查看它的项目地址 openai-whisper。\n字幕是否翻译依据的是项目创建时的配置。但如果项目创建时 --translator 指定了翻译器，但不希望翻译字幕，可通过如下选项单独配置：\n--translator，指定翻译器，默认值取决于项目配置； --translator-from-lang，用于指定翻译的原始语言，默认为 en； --translator-to-lang，用于指定翻译的目标语言，默认为 zh； 视频合成 # 视频合成命令如下所示，将会按项目的默认配置将音视频、字幕合成到成一个成品视频。\nmvideo make 如果希望在视频中增加封面、开始声明和结尾提醒，执行如下命令：\nmvideo make --cover-text \u0026#34;封面标题\u0026#34; --declaim-text \u0026#34;特别说明：本频道专注于分享优质视频。\u0026#34; --end-text \u0026#34;感谢敢看，如您喜欢本视频，欢迎点赞、收藏、评论与关注 \u0026#34; --without-chapter --cover-text，封面标题，视频开头，封面图存放于 output-path 目录下，如未指定则不制作； --declaim-text，声明文字，声明文本，用于生成视频声明，未指定则无声明； --end-text，结尾文本，用于感谢观众或提醒点赞关注等，未指定则无结尾提醒； 多视频合成时，默认会使用视频标题作为章节标题，如要禁用，可通过如下命令实现：\nmvideo make --without-chapter --without-chapter，禁用默认的章节提示； 视频合成的默认行为是，无论当前是否存在成品视频，都会重新合成视频。如想只在无成品视频的情况下才合成，使用如下命令：\nmvideo make --smart --smart，存在成品视频，跳过不合成，否则合成视频； 字幕合成配置，默认按项目配置，如果原始字幕和翻译字幕都存在，将会全部合成到视频中。\n通过如下命令指定配置：\nmvideo make --subtitle \u0026#39;none\u0026#39; --subtitle 指定合成的字幕，none-无，origin-原语言，translate-翻译，all-所有； 视频发布 # 视频发布，执行如下命令，会打开浏览器自动执行发布操作。\nmvideo publish --platform bilibili --platform，指定视频的发布平台，默认值为 bilibili 平台，当前只支持 bilibili； 执行发布的详细信息，如下所示：\nmvideo publish --platform bilibili --title \u0026#34;发布视频主题\u0026#34; --keywords \u0026#34;程序员,编程\u0026#34; --source-url \u0026#34;https://youtube.com/watch?v=xxx\u0026#34;\u0026#34; title，指定视频发布标题，默认使用第一个视频标题，启用翻译的话，使用翻译版本； source-url，指定搬运来源，默认值，urls 的第一个视频地址或 playlist 即 playlist 地址； keywords，指定视频标签，默认使用 youtube 视频的第一个关键字； 最后 # 本文介绍了 YouTube 视频搬运的整体方案，下期视频将集中于介绍资源的下载。\n我的博文：基于 Python 视频搬运工具开发 Part 1\n","date":"2023-11-27","externalUrl":null,"permalink":"/posts/2023-11-27-automatic-transfer-from-youtube-using-python/","section":"文章","summary":" 本文介绍如何基于 Python 实现的从 YouTube 自动化搬运视频到国内平台的命令行工具，计划命名为 mvideo，即 move video 的意思。\n","title":"基于 Python 视频搬运 Part 1 - 先导篇","type":"posts"},{"content":"本文开始，我将用一个系列介绍如何高效使用 Tmux。\n关于 Tmux 教程有不少，有一些写的非常不错，我也来尝试下这个主题吧。\n本篇博文是系列第一篇，目标是介绍我使用 Tmux 的快速一览。我将只演示效果，不介绍细节。后续文章再逐步介绍，打造一套高效的 Tmux 工作环境。\n何为 Tmux？ # Tmux 是一款终端复用器，即 terminal multiplexer，它能实现将会话与终端的解绑，同时支持管理多个会话和窗口。\n从如上的介绍中，能了解到 tmux 的两个核心能力，即会话与终端的解绑、会话窗口的管理。\n快速安装 # ❯ brew install tmux 验证安装是否成功：\n❯ tmux -V tmux 3.3a 会话与终端解绑 # 简言之，即使终端窗口关闭，如果 tmux 没有停止则会话不停。\n是不是想到了另外一个类似能力的 Shell 命令 - screen？但 Tmux 比它更加强大。\n基于 Tmux 的这个能力，我们可以将一些后台任务放在 tmux 中进行，如此一来，即使如 ssh 断连，任务也还在继续运行。\n演示案例，使用 tmux 命令开启一个 tmux 会话，执行 top 命令，detach 会话后，重新使用 tmux attach 进入会话。\n效果如下所示：\n我们会发现 top 命令还在运行中。\n会话窗口的管理 # Tmux 支持同时开启多个会话和窗口，实现一个终端多会话多窗口的效果。\n利用 Tmux 的这个特点，我们可以创建一些会话面板，长期运行多个任务，如创建一些监控面板。\n再者，如果你习惯于在 terminal 上 coding，还可通过这种方式管理常驻我们的日常开发工作区。\nTmux 与 Vim 集成 # 对于平时使用 Vim 或 Neovim 为编程环境的朋友而言，Tmux 与 Vim 的集成能最大化提高日常工作的生产力。\n如我们可以将两者的导航快捷键打通，实现 CTRL+hjkl 组合键在 Vim 和 Tmux 的 panes 之间的快捷跳转。\n剪贴板共享 # 我们还可以将 Vim、Tmux 甚至系统的剪贴板打通，继续将我们的的 CV 大法发扬广大。\n补充说明，Tmux 如果没有配置好，使用起来很别扭，如会话窗口间的切换、拷贝粘贴、缩放等能力，还有默认的快捷键配置，要不是使用体验差，要不就是不支持。\n估计有不少朋友在简单用了 Tmux 几天就放弃了，或只是当成 screen 使用。\n窗口管理器 tmuxifier # 对于一些固定的会话窗口布局，我们还可以通过类似于 tmuxifier 或 tmuxinator 以配置方式固定布局，这样即使 tmux 关闭重启，也可一键开启窗口布局。\n一个示例，配置一个博客写作工作空间，效果如下所示：\n配置一个主的 pane 用于 vim 编写博客，底部的两个 pane，一个用于执行命令，一个用于运行启动 hugo server 实时查看博文实时效果。类似于 web 开发中常用的三个 pane。\n窗口布局持久化 # Tmux 支持插件机制，同样有会话持久化的插件。它能实现在 tmux 重启后，恢复会话的最新状态。\n不过，我确实不喜欢这种方式，电脑或服务器重启后，我希望由我决定重新开启什么样的环境。否则，久而久之，持久化的会话可能很多，杂乱无章。\n快速配置方式 # 对于懒人而言，如果我就是不想花时间去研究这些配置，只希望有一个现成的配置，知道如何使用就行了，该如何做呢？不少人其实并不想了解复杂的配置。但问题是，不配置的，Tmux 的默认配置确实是难以上手。\n网上有人会提供了现成的配置，较流行的就是这个 oh-my-tmux，访问 GitHub 地址 oh-my-tmux。它默认配置出了一个易用的 Tmux 版本，前面我们提到的能力，基本在 oh-my-tmux 都有提供，如果希望自定义，也可很方便地修改。\n我的博文：我的终端环境：Tmux Part1 - 快速一览 和 Tmux 官方文档：Tmux official documentation\n","date":"2023-11-17","externalUrl":null,"permalink":"/posts/2023-11-18-tmux-part1-basic/","section":"文章","summary":"本文开始，我将用一个系列介绍如何高效使用 Tmux。\n关于 Tmux 教程有不少，有一些写的非常不错，我也来尝试下这个主题吧。\n本篇博文是系列第一篇，目标是介绍我使用 Tmux 的快速一览。我将只演示效果，不介绍细节。后续文章再逐步介绍，打造一套高效的 Tmux 工作环境。\n","title":"我的终端环境：Tmux Part1 - 快速一览","type":"posts"},{"content":"本文介绍如何基于 GitHub 为图片存储，通过 API 随机返回可用的图片地址。\n之所以研究它，主要是为了省钱，毕竟用啥 S3、七牛云、阿里云都是要花钱的。这套思路，gitee 应该也可以的，不过我看网上说，gitee 禁止图床开源啥的。而开发随机图片 API 只是为了验证是否能通过 GitHub 的 API 获取仓库中的文件，支持进一步开发其他管理工具。\n前言 # 平时常用的桌面壁纸、终端背景图片，亦或是博客背景或文章封面，这些都离不开图片。于是，如何就想如何免费管理这些图片。\n在网上找了一些免费的随机图片 API，大部分处于不可用的状态，或者是需要注册登录，创建 API Token。\n作为一名老年程序员，自然就想能通过编程实现，实现图片自由。虽然也可以通过类似爬虫的思路实现，但还是希望都在自己的控制中，万一出现不好的图片就不好了。\n免费 CDN 加速 # 我的博客图片一直在用 GitHub 存储，通过 jsdelivr CDN 加速。于是就思考，如果能获取到 GitHub 存储的文件列表，就可以实现一个图片服务。\n简单说下 jsdelivr CDN，它支持对 GitHub 中文件的加速访问。如位于我的仓库下的图片，通过对地址转为为 jsdelivr CDN 地址。\n如下的地址：\nhttps://github.com/poloxue/public_images/default/0001.webp 通过如下地址访问：\nhttps://cdn.jsdelivr.net/gh/poloxue/public_images@latest/default/0001.webp 现在如果能顺利获取到仓库的图片文件列表，即可将 github 作为我们的图片图片存储，而无需花钱购买云存储实现。\n查询 GitHub 图片列表 # 如何获得 GitHub 文件列表呢？这篇文章是讲如何将 GitHub 作为存储使用的，肯定要支持查询的，否则就没法玩了。\nGitHub 支持接口获取仓库文件列表，基本流程是先通过某个接口查询仓库的信息，其中某个字段包含了最新的 hash，我们通过调用这个 hash，就能拿到这个 hash 下的文件列表。\n仓库信息查看，即第一个接口，如下所示，如查询 user/repo 下某分支的情况。\ncurl https://api.github.com/repos/{user}/{repo}/branches/{branch}。 JSON 返回体中，通过访问路径 .commit.commit.tree.url 拿到获取仓库文件列表的接口地址。其实主要是获取该分支最近的 commit hash。\n演示案例，获取 github.com/poloxue/public_images\n通过 httpie 执行请求，如下所示：\n$ curl https://api.github.com/repos/poloxue/public_images/branches/main { // ... \u0026#34;commit\u0026#34;: { \u0026#34;commit\u0026#34;: { \u0026#34;tree\u0026#34;: { \u0026#34;sha\u0026#34;: \u0026#34;3859a482b15ed41bfb86ce073d6c500fef36910c\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://api.github.com/repos/poloxue/public_images/git/trees/3859a482b15ed41bfb86ce073d6c500fef36910c\u0026#34; } } } } 通过 jq 解析请求结果，再次通过 httpie 请求，命令如下：\ncurl $(curl https://api.github.com/repos/poloxue/public_images/branches/main | jq -r \u0026#39;.commit.commit.tree.url+\u0026#34;?recursive=1\u0026#34;\u0026#39;) | jq \u0026#39;.tree[].path\u0026#39; 如上的命令中通过 ?recursive=1 实现遍历子目录，通过 \u0026lsquo;.tree[].path\u0026rsquo; 返回所有文件和目录。\n返回结果如下：\n.gitignore README.md beauties beauties/0001.jpeg beauties/0002.jpeg default default/0001.webp default/0002.webp scenes scenes/0001.webp scenes/0002.webp 特别说明：接口的返回其实有数量限制，但这个限制并不是很大，个人使用无需担心。如果想扩展扩展容量，可考虑创建多个分支，或者多个仓库啥的。\n我的基本原则，把白嫖贯彻到底。\n图片 API 服务 # 在了解如何使用GitHub 的接口后，我通过 gin 开发一个随机 API，创建了一个简单的 Random Image API。实现方案也不复杂，通过 GitHub 接口获取图片列表，随机选择其中一张图片，把它在仓库中的路径与 jsdelivr CDN 地址拼接，随机返回一个图片地址。\n接口定义：\nPath: /image/random/{category} Parameters： category：str, 图片类型，即上面 github 仓库的子目名称； Return： image：str，图片地址，指定 category 类型下的一个图片地址； 接下来，我就来尝试实现这个服务。\nmain 函数 # 我直接使用 gin 框架开发这个 demo API。main 入口代码如下所示：\nvar imageContainer imageContainer func init() { imageContainer = NewImageContainer() } func GetRandomImage(ctx *gin.Context) { category := c.Param(\u0026#34;category\u0026#34;) imageURL, err := imageContainer.RandomImage(category) // ... c.JSON(http.StatusOK, gin.H{\u0026#34;image\u0026#34;: imageURL}) } func main() { router := gin.Default() router.GET(\u0026#34;/image/random/:category\u0026#34;, GetRandomImage) router.Run(\u0026#34;:8080\u0026#34;) } gin 创建和路由的部分没啥可介绍的。重点是，我创建了一个 ImageContainer 用于连接 GitHub 随机拿到随机的图片地址。\n为什么我把 imageContainer 声明为全局变量，主要想着这样设计个本地缓存更容易些，不然一直请求 GitHub API 可不是个好事情，容易触发 GitHub 请求p频率限制。\nImageContainer # 首先，定义下 ImageContainer 类型和它的创建函数，代码。\n如下所示：\ntype ImageContainer struct { repo string branch string images map[string][]string } func NewImageContainer(repo, branch string) *ImageContainer { return \u0026amp;ImageContainer{ repo: repo, branch: branch, } } 构造函数 NewImageContainer 创建时，要指定仓库 repo 和分支 branch。ImageContainer 中的 images 字段用于按分类保存图片列表。对于 API 场景，使用 sync.Map 是个更好的选择，因为会存在并发竞争的问题。\n随机图片的获取流程可描述为三个步骤，分别是查询最新 commit hash、基于 commit hash 获取 trees 图片列表和从图片列表中随机拿到一张图片返回。\n随机图片 API 的核心部分代码，如下所示：\nfunc (s *ImageContainer) RandomImage() { lastHash, err := s.LastHash() if err != nil { return \u0026#34;\u0026#34;, err } images, err := s.QueryImages(category, lastHash) if err != nil { return \u0026#34;\u0026#34;, err } if len(images) == 0 { return \u0026#34;\u0026#34;, errors.New(\u0026#34;No image found\u0026#34;) } return images[rand.Intn(len(images))], nil } 其他部分如 LastHash、QueryImages 函数代码比较啰嗦，就不介绍了。不喜欢把太丑的代码贴在文章里。有兴趣可查看源码仓库 poloxue/random_image_from_github。\n测试效果 # 请求示例，如下所示：\ncurl http://localhost:8080/image/random/scenes 输出结果：\n{ \u0026#34;image\u0026#34;: \u0026#34;https://cdn.jsdelivr.net/gh/poloxue/public_images@latest/scenes/0005.webp\u0026#34; } 这只是服务的最小版本，还可以继续扩展，提供更多接口能力，甚至可以上传下载图片，还有生成缩略图等等。本案例只是测试代码，很多场景是没有实现的。\n总结 # 本文介绍了如何基于 GitHub 实现一个简单的 random image API 服务，主要还是展示将 GitHub 作为替代 S3 存储服务使用。对于个人而言，肯定是能白嫖，就不要花钱，哈哈。\n因为 GitHub 其实是提供了完整一套的增删改查接口的，基于它，我可以开发一个图床管理 app。还有，进一步，还可以基于它开发如 neovim、vscode 的博客图片插件，提升 markdown 写作效率。\n我的博客地址：以 GitHub 作为图片存储创建随机图片 Service API\n","date":"2023-11-17","externalUrl":null,"permalink":"/posts/2023-11-17-build-a-random-image-api-using-github/","section":"文章","summary":"本文介绍如何基于 GitHub 为图片存储，通过 API 随机返回可用的图片地址。\n之所以研究它，主要是为了省钱，毕竟用啥 S3、七牛云、阿里云都是要花钱的。这套思路，gitee 应该也可以的，不过我看网上说，gitee 禁止图床开源啥的。而开发随机图片 API 只是为了验证是否能通过 GitHub 的 API 获取仓库中的文件，支持进一步开发其他管理工具。\n","title":"我用 GitHub 作为存储开发了一个随机图片 API","type":"posts"},{"content":" 本文接着上文，将介绍如何使用 fetch 配置更加丰富地终端启动消息。\n前言 # 你是否在终端上看到过类似如下的信息？\n我在刚开始讲终端环境这个系列，就有小伙伴在我的视频下 show 了他的终端。\n要实现这个终端效果，要依赖一种名为 fetch 的程序。\n系列阅读：\n我的终端环境：iTerm2 的安装与体验 我的终端环境：zsh 安装与主题，推荐 7 个提升效率的 zsh 插件 我的终端环境：6 个强大的 zsh 插件 我的终端环境：与众不同的 zsh 主题 - powerlevel10k 我的终端环境：高效 shell 命令（一）之目录文件命令 - exa、zoxide 与 bat 我的终端环境：高效 shell 命令（二）之高效查找与搜索 - fd ripgrep fzf 我的终端环境：高效 shell 命令（三）之提效 web 开发 - entr httpie jq 我的终端环境：高效 shell 命令（四）之20+1 个 modern-unix 命令 我的终端环境：终端启动消息 - ASCII art 我的终端环境：终端启动消息 - pfetch/neofetch/fastfetch 更多待续\u0026hellip;\n什么是 fetch？ # 所谓 fetch，是指一类系统信息收集的脚本，显示系统摘要信息（软硬件信息），例如发行版、内核、版本、桌面环境、窗口管理器等。fetch 主要是在系统的终端上使用，显示我们的工作环境。\n其中，较为出名的 fetch，有诸如 neofetch、pfetch、fastfetch 等。\n一键安装 # brew instal pfetch neofetch fastfetch imagemagick w3m 其中，imagemagick 和 w3m 是 neofetch 的依赖，要单独安装，否则无法正常显示图片。\n更多 fetch # 如果希望发现更多关于 fetch 的资源，查看 awesome-fetch。没错，fetch 这个系列也有一个 awesome 系列。\n开始正文 # 让我们开始正式介绍 fetch 的使用方法。\n今天呢，主要介绍三个 fetch， 它们分别是 pfetch 、neofetch 和 fastfetch，有的追求简洁、有的强大但复杂，有的高性能。\n总之，它们各具特色。\npfetch # pfetch 的目标是通过 sh 实现一个简洁的系统信息收集工具。它支持 Linux、Android、BSD、Windows、MacOS、Solaris 等系统。\n使用 # 输入以下命令查看效果：\npfetch 效果如下：\n配置 # 如上图打印的信息中，分为两个部分，系统 Logo 和系统软硬件信息。pfetch 使用环境变量配置具体显示什么信息。\n如只显示 ASCII Logo，设置环境变量：\nexport PF_INFO=\u0026#34;ascii\u0026#34; 如不显示 ASCII Logo，设置环境变量：\nexport PF_INFO=\u0026#34;title os host kernel uptime pkgs memory\u0026#34; 或者修改默认的 ASCII Logo 为 Linux：\nexport PF_ASCII=\u0026#34;linux\u0026#34; 更多配置项，自行查看 pfetch 仓库文档。\nneofetch # neofetch 是一款用 bash script 实现，快速且高度可定制的 fetch。它的功能比 pfecth 强大的多，支持展示 ASCII 和 图片，在 Linux、BSD、Mac OS X 和 Window 等系统上可运行。\n不得不说，它的 github star 数量也是所有 fetch 中最多的。大部分人一般用的都是它吧。\n要说它的缺点，一是性能低，因为是 bash script 实现，性能不高；二是已停止维护，好像 bug 已停止修复。\n使用 # 我将以 Mac OS X 演示其使用。\n简单实用，直接输入以下命令：\n\u0026gt; neofetch 效果如下：\n如上所示，和 pfetch 一样，两部分组成，一部分显示系统 Logo（Mac OS X），另一部分显示系统摘要信息。\n配置 # neofetch 功能强大，高度可定制，可通过命令选项或者配置文件进行定制，它的默认配置文件位于 ~/.config/neofetch/config.conf 中。\n不显示 ASCII Logo。\nimage_backend=\u0026#34;off\u0026#34; # 或 nefetch --off 如显示字段定制：\nprint_info() { info title info underline info \u0026#34;OS\u0026#34; distro info \u0026#34;Host\u0026#34; model info \u0026#34;Kernel\u0026#34; kernel info \u0026#34;Uptime\u0026#34; uptime info \u0026#34;Packages\u0026#34; packages info \u0026#34;Shell\u0026#34; shell # info \u0026#34;Resolution\u0026#34; resolution # info \u0026#34;DE\u0026#34; de # info \u0026#34;WM\u0026#34; wm # info \u0026#34;WM Theme\u0026#34; wm_theme # info \u0026#34;Theme\u0026#34; theme # info \u0026#34;Icons\u0026#34; icons # info \u0026#34;Terminal\u0026#34; term # info \u0026#34;Terminal Font\u0026#34; term_font # info \u0026#34;CPU\u0026#34; cpu # info \u0026#34;GPU\u0026#34; gpu info \u0026#34;Memory\u0026#34; memory info cols } 注释掉如上任一字段裁剪显示的字段。\n或替换默认的 ASCII art 为其他 Logo，如替换为 ubuntu Logo，虽然，这么做不太合适。\nascii_distro=\u0026#34;ubuntu\u0026#34; # 或 neofetch --ascii_distro=ubuntu 同样，也可以将上篇文中的 cowsay 配置为 ASCII 部分的显示内容。\nimage_source=\u0026#34;$(fortune | cowsay -W 40)\u0026#34; # 或 neofetch --source \u0026#34;$(fortune | cowsay)\u0026#34; neofetch 还支持将左侧的内容从 ASCII art 替换为图片。\n在 iterm2 的设置方式：\nimage_backend=\u0026#34;iterm2\u0026#34; image_source=\u0026#34;$HOME/Pictures/avatar-transparency.png\u0026#34; # 或 neofetch --iterm2 ~/Pictures/avatar-transparency.png 一个问题，neofetch 有些场景下会无缘无故打印很多空行，要通过命令选项 --size 或配置参数 image_size 实现图片大小固定，同时再利用 yoffset 和 gap 调整出一个比较好看的效果。\n如下所示：\nneofetch 有一个问题，因为使用 bash script 实现，性能一般，明显能感觉到 info 打印时的卡顿。我们可通过 info \u0026quot;OS\u0026quot; distro \u0026amp; 的形式调用，即 \u0026amp; 实现异步执行，再利用 wait 等待，提升性能。\nfastfetch # 和 pfetch、neofetch 不同，fastfetch 是 C 语言实现，它的性能自然比之前两者高上很多，而且能力不次于 neofetch，而且最近还在积极开发中。\n使用 # 输入命令查看效果：\nfastfetch 配置 # 默认配置文件在 ~/.config/fastfetch/config.jsonc。\n修改配置文件，将配置中的模块移除掉对应模块即可。\n{ \u0026#34;modules\u0026#34;: [ \u0026#34;title\u0026#34;, \u0026#34;separator\u0026#34;, \u0026#34;os\u0026#34;, \u0026#34;host\u0026#34;, \u0026#34;uptime\u0026#34;, \u0026#34;packages\u0026#34;, \u0026#34;shell\u0026#34;, \u0026#34;memory\u0026#34;, \u0026#34;disk\u0026#34;, \u0026#34;localip\u0026#34;, \u0026#34;battery\u0026#34;, \u0026#34;poweradapter\u0026#34;, \u0026#34;locale\u0026#34;, \u0026#34;break\u0026#34;, \u0026#34;colors\u0026#34; ] } 只展示摘要信息，命令如下：\nfastfetch --logo none 与 fortune | cowsay | lolcat 结合，如下所示：\nfastfetch --data-raw \u0026#34;$(fortune | cowsay -W 30 | lolcat -f)\u0026#34; 显示图片，在 iterm2 的命令如下所示：\nfastfetch --logo ~/Pictures/avatar-transparency.png --logo-type iterm --logo-width 30 --logo-height 15 相对于 neofetch，fastfetch 不支持 neofetch 的图片处理能力。\n总结 # 本文介绍了 pfetch / neofetch / fastfetch 的使用，如果追求简洁可使用 pfetch，追求性能可使用 fastfetch，或者功能最丰富的 neofetch。\n我的博文地址：我的终端环境：terminal 启动消息 - pfetch/neofetch/fastfetch\n","date":"2023-11-14","externalUrl":null,"permalink":"/posts/2023-11-16-beautify-your-terminal-welcome-using-fetch/","section":"文章","summary":" 本文接着上文，将介绍如何使用 fetch 配置更加丰富地终端启动消息。\n","title":"我的终端环境：终端启动消息 - pfetch/neofetch/fastfetch 教程","type":"posts"},{"content":" 本文介绍如何设置 MacOS 系统的终端启动消息，或者说欢迎消息。\n本文介绍的内容同样适用于其他类 Unix 系统。\n某种意义上，这是一个无用的小知识，但它确实很有趣。毕竟，不是任何事情都要追求所谓价值，有趣也挺重要的。\n登录消息 # 每天打开 terminal 终端，系统默认会打印一串的消息，如 \u0026ldquo;Last Login xxx\u0026rdquo; 之类的消息。是否想过让这个默认消息更加丰富一些？\n如 MacOS 这样的类 Unix 系统默认有两种方式，一种是基于系统的 motd，另一种是通过启动脚本打印消息。\n系列阅读：\n我的终端环境：iTerm2 的安装与体验 我的终端环境：zsh 安装与主题，推荐 7 个提升效率的 zsh 插件 我的终端环境：6 个强大的 zsh 插件 我的终端环境：与众不同的 zsh 主题 - powerlevel10k 我的终端环境：高效 shell 命令（一）之目录文件命令 - exa、zoxide 与 bat 我的终端环境：高效 shell 命令（二）之高效查找与搜索 - fd ripgrep fzf 我的终端环境：高效 shell 命令（三）之提效 web 开发 - entr httpie jq 我的终端环境：高效 shell 命令（四）之20+1 个 modern-unix 命令 我的终端环境：终端启动消息 - ASCII art 我的终端环境：终端启动消息 - pfetch/neofetch/fastfetch 更多待续\u0026hellip;\nmotd # 首先，通过 motd 的实现方案。motd，即 message of today，类 Unix 系统基本都支持，我们只要将启动消息写入到 /etc/motd中，即可定制终端启动消息。\n对于公用服务器，系统管理员用它可向普通用户发送消息。而且，有部分 Linux 发行版甚至用它的默认配置给用户发送广告。\n演示看效果吧。\n将文字 \u0026ldquo;Hello World. Have a nice day!\u0026rdquo; 写入 /etc/motd 中，开启一个新的终端。\nmotd 的缺点是它是静态内容，如果要更新内容，则需编辑文件。\nmotd 的动态效果实现方案，可基于 crontab 定时更新 motd 中的内容实现 motd 伪动态。不过，这种方式主要是适合于不停机的服务器，对于个人电脑，经常处于关机状态，可能不太适合。\n启动脚本 # 实现真正意义上的动态 motd 消息，可直接通过系统启动脚本打印环境消息，如在 ~/.zshrc 中添加欢迎消息打印脚本，消息内容可任意定制。\n如 MacOS 系统在启动时，通过解析 uname -a 输出内容，打印系统信息和内核版本：\necho `uname -a | cut -d\u0026#39; \u0026#39; -f1 -f3` 加入到 ~/.zshrc 文件中，效果如下所示：\n到这里，终端欢迎消息定制的基本内容介绍结束。但问题是，这明显不够惊艳，对于这种无用小知识，肯定要 \u0026ldquo;华而不实\u0026rdquo;，但现在没有看到任何惊艳之处。\nTo be continued\u0026hellip;\nASCII art 定制欢迎消息 # 文字单一枯燥，如何给欢迎消息引入不一样的内容？我们将引入 ASCII art 改造枯燥的启动罅隙。\n所谓 ASCII art，是通过 ASCII 字符表达图片的一种方式。在文字比图像更稳定的场合，如终端、或者早起没有图像显示能力的设备，ASCII art 显然是更好的选择。\n示例如下：\n_ _ _ _ _ | |__ ___| | | ___ __ _____ _ __| | __| | | \u0026#39;_ \\ / _ \\ | |/ _ \\ \\ \\ /\\ / / _ \\| \u0026#39;__| |/ _` | | | | | __/ | | (_) | \\ V V / (_) | | | | (_| | |_| |_|\\___|_|_|\\___/ \\_/\\_/ \\___/|_| |_|\\__,_| __________________ \u0026lt; have a nice day! \u0026gt; ------------------ \\ ^__^ \\ (oo)\\_______ (__)\\ )\\/\\ ||----w | || || 如何生成 ASCII art？\nASCII art 一般有 ASCII text 和 ASCII image 两种形式。可通过在线站点生成，或通过一些命令生成。\n在线网站 # 推荐两个工具站点，ASCII Generator 和 ASCII banner。\n演示效果，使用 ASCII Generator 生成 ASCII 3D 文字，如下所示：\n如想找一些现成的 ASCII art，查看 ascii-art，其中有一些现成的可供选择。\n命令工具 # 举一反三，工具站点能生成 ASCII art，那必然通过命令也可以做到，必须都是程序实现。\nMacOS 一键安装：\nbrew install figlet cowsay fortune lolcat figlet 生成 ASCII text\nfiglet，可用于 ASCII text。它常用于开源项目生成 text banner。\n使用演示：\n\u0026gt; figlet hell world _ _ _ _ _ | |__ ___| | | ___ __ _____ _ __| | __| | | \u0026#39;_ \\ / _ \\ | |/ _ \\ \\ \\ /\\ / / _ \\| \u0026#39;__| |/ _` | | | | | __/ | | (_) | \\ V V / (_) | | | | (_| | |_| |_|\\___|_|_|\\___/ \\_/\\_/ \\___/|_| |_|\\__,_| fortune + cowsay 牛说名言\n对于 ASCII art 内容，还有常用的一个组合命令：fortune + cowsay。\nfortune 用于生成随机生成 \u0026ldquo;英文名言\u0026rdquo;。\n❯ fortune Things will get better despite our efforts to improve them. -- Will Rogers 如果希望有中文内容，可安装阮一峰老师提供的 fortune 中文数据库。\ncowsay，即 \u0026ldquo;牛说\u0026rdquo; 的意思，配合 fortune，会生成一个 \u0026ldquo;牛说某个格言\u0026rdquo; 效果。\n❯ fortune | cowsay ________________________________________ / Shame is an improper emotion invented \\ | by pietists to oppress the human race. | | | | -- Robert Preston, Toddy, | \\ \u0026#34;Victor/Victoria\u0026#34; / ---------------------------------------- \\ ^__^ \\ (oo)\\_______ (__)\\ )\\/\\ ||----w | || || 默认的 \u0026ldquo;牛\u0026rdquo; 的形象是可修改的，如换成 \u0026ldquo;羊\u0026rdquo;，通过 -f 修改。\n命令如下：\n❯ fortune | cowsay -f sheep ________________________________________ / Winny and I lived in a house that ran \\ | on static electricity... If you wanted | | to run the blender, you had to rub | | balloons on your head... if you wanted | | to cook, you had to pull off a sweater | | real quick... | | | \\ -- Steven Wright / ---------------------------------------- \\ \\ __ UooU\\.\u0026#39;@@@@@@`. \\__/(@@@@@@@@@@) (@@@@@@@@) `YY~~~~YY\u0026#39; || || 要查看支持的所有形象，通过如下命令：\n\u0026gt; cowsay -l Cow files in /usr/local/Cellar/cowsay/3.04_1/share/cows: beavis.zen blowfish bong bud-frogs bunny cheese cower daemon default dragon dragon-and-cow elephant elephant-in-snake eyes flaming-sheep ghostbusters head-in hellokitty kiss kitty koala kosh luke-koala meow milk moofasa moose mutilated ren satanic sheep skeleton small stegosaurus stimpy supermilker surgery three-eyes turkey turtle tux udder vader vader-koala www 如果想定制更多形象，可参考上面目录 /usr/local/Cellar/cowsay/3.04_1/share/cows 中的案例，通过 -f 执行也是可以的。\nlolcat - 随机彩虹着色\nlolcat，可用于给输出进行随机的彩虹着色，如给以上输出 ASCII 文字着色，可通过 lolcat 实现随机彩虹效果，效果如下：\n或给 cowsay 的输出着色，效果如下：\n程序实现\n如果想通过编程实现文字转 ASCII 或图片也是可以做到的。当然，其实也没啥技术含量，依赖的 Python 的强大的三方库支持。\n文字转 ASCII，可通过 pyfiglet 库实现，而图片可以来 pillow 库实现。\npip install pyfiglet pip install pillow pip install click 完整代码的 gist 地址：ascii-generator。粘贴一份完整代码，如下所示：\nimport sys import click import pyfiglet import PIL.Image def image_to_ascii(path): try: img = PIL.Image.open(path) except: print(path, \u0026#34;Unable to find image\u0026#34;) exit(1) width, height = img.size aspect_ratio = height / width new_width = 50 new_height = aspect_ratio * new_width * 0.55 img = img.resize((new_width, int(new_height))) img = img.convert(\u0026#34;L\u0026#34;) chars = [\u0026#34; \u0026#34;, \u0026#34;J\u0026#34;, \u0026#34;D\u0026#34;, \u0026#34;%\u0026#34;, \u0026#34;*\u0026#34;, \u0026#34;P\u0026#34;, \u0026#34;+\u0026#34;, \u0026#34;Y\u0026#34;, \u0026#34;$\u0026#34;, \u0026#34;,\u0026#34;, \u0026#34; \u0026#34;] pixels = img.getdata() new_pixels = [chars[pixel // 25] for pixel in pixels] new_pixels = \u0026#34;\u0026#34;.join(new_pixels) new_pixels_count = len(new_pixels) ascii_image = [ new_pixels[index : index + new_width] for index in range(0, new_pixels_count, new_width) ] return \u0026#34;\\n\u0026#34;.join(ascii_image) def text_to_ascii(text): return pyfiglet.figlet_format(text) @click.command() @click.option( \u0026#34;--type\u0026#34;, \u0026#34;type_\u0026#34;, default=\u0026#34;text\u0026#34;, help=\u0026#34;source type, unavailable options: text, image\u0026#34;, ) @click.option(\u0026#34;--source\u0026#34;, help=\u0026#34;text or image path\u0026#34;) def main(type_, source): if type_ == \u0026#34;text\u0026#34;: ascii = text_to_ascii(source) elif type_ == \u0026#34;image\u0026#34;: ascii = image_to_ascii(source) else: return print(ascii) if __name__ == \u0026#34;__main__\u0026#34;: main() 其中的参数如宽度还有字符，都是可调节的，使用起来也都非常简单。\n文字转 ASCII：\n\u0026gt; python ascii_image.py --type text --source \u0026#34;hello world\u0026#34; _ _ _ _ _ | |__ ___| | | ___ __ _____ _ __| | __| | | \u0026#39;_ \\ / _ \\ | |/ _ \\ \\ \\ /\\ / / _ \\| \u0026#39;__| |/ _` | | | | | __/ | | (_) | \\ V V / (_) | | | | (_| | |_| |_|\\___|_|_|\\___/ \\_/\\_/ \\___/|_| |_|\\__,_| 图片转 ASCII：\n完成设置 # 静态内容，现在只要将生成的 ASCII art 写入到 motd 文件。如果希望静态文本中的输出保持颜色，可通过 lolcat -f 将颜色信息也导入到文本中。\n动态内容，将类似于 fortune | cowsay | locat 加入到启动脚本 ~/.zshrc 即可。\n到此，初步大功搞成。\n总结 # 通过本文，我的奇怪知识又增加了。对了，文中的 ASCII image 的原图是我的博客头像。\n最后，下期介绍如何在欢迎消息中展示更丰富的信息，如：\n我的博文：我的终端环境：无用小知识 - 终端启动消息配置\n","date":"2023-11-13","externalUrl":null,"permalink":"/posts/2023-11-15-beautify-your-terminal-welcome-message/","section":"文章","summary":" 本文介绍如何设置 MacOS 系统的终端启动消息，或者说欢迎消息。\n","title":"我的终端环境：终端启动消息 - ASCII art","type":"posts"},{"content":"","date":"2023-11-06","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2023-11-06","externalUrl":null,"permalink":"/tags/zsh/","section":"Tags","summary":"","title":"Zsh","type":"tags"},{"content":" 本篇文章是介绍 modern-unix 仓库剩余的 20 个命令的上篇，外加 1 比 modern-unix 中更易于使用的命令。\n系列阅读：\n我的终端环境：iTerm2 的安装与体验 我的终端环境：zsh 安装与主题，推荐 7 个提升效率的 zsh 插件 我的终端环境：6 个强大的 zsh 插件 我的终端环境：与众不同的 zsh 主题 - powerlevel10k 我的终端环境：高效 shell 命令（一）之目录文件命令 - exa、zoxide 与 bat 我的终端环境：高效 shell 命令（二）之高效查找与搜索 - fd ripgrep fzf 我的终端环境：高效 shell 命令（三）之提效 web 开发 - entr httpie jq 我的终端环境：高效 shell 命令（四）之20+1 个 modern-unix 命令 我的终端环境：终端启动消息 - ASCII art 我的终端环境：终端启动消息 - pfetch/neofetch/fastfetch 更多待续\u0026hellip;\n命令集合 # 第一篇文章中推荐一个 github 仓库：modern-unix，其中收录了大量的更具现代风格的命令。例如，最常用的命令，如 ls、cd、grep、find 等等命令，这个仓库都提供了合适的替代命令。\n针对我们日常工作最常用的命令，我已用了三篇文章，从不同场景角度出发，介绍了它们的使用，从而提升终端的使用效率。毫无疑问，这些命令更具现代风格。\n除前面已经介绍的命令，本文将会极简的方式介绍下剩余的其他命令。\n一键安装 # 一键安装剩余的 20 + 1 (lf) 个命令，如下所示：\nbrew install lsd git-delta dust duf broot ag mcfly choose-rust sd cheat tldr bottom glances gtop hyperfine gping procs curlie xh dog lf lsd # lsd，号称 \u0026ldquo;下一代 ls 命令\u0026rdquo;，算是对 GNU ls 的重写，且与 ls 兼容，和 exa 功能上类似。\nlsd --long --header --git delta # delta，可用于支持 git 、diff 和 git grep 的语法高亮和分屏对比；\n与 diff 一起使用：\ndiff -u main1.go main2.go | delta 与 git diff 一起使用\ngit show dust # dust - 使用 rust 实现，du+rust = dust，更直观的 du 命令。默认行为，以找到最大文件为第一选择。\nduf # duf - 视觉体验更佳 df，可作为 df 的替代品，按类型分组展示。\nbroof # broot - 终端文件浏览器，类似于 mac 的 finder 的终端版本。\n我觉得，如果说到命令行文件浏览器，lf 体验更佳，是一个更不错的选择，比起 broot，支持的 vim 方式导航和搜索。有兴趣也可以了解下。\nag # ag - 类似于 ack 的代码搜索工具，但搜索速度更快。其实，和 rg 有点类似，但做了个压测，性能没有 rg 优秀。\nmcfly # mcfly - mcfly 智能搜索引擎取代 CTRL-R 默认的搜索引擎，会考虑你的工作环境和历史命令等，通过一个小型网络进行优先级排序。\nchoose # choose - 快速且易于使用的 cut 命令。\nsd # sd - 更直观的 \u0026ldquo;选择替换\u0026rdquo; 命令，可用于替换 sed。\nsd old new filename cheat # cheat - 是 unix 命令的备忘录，是一个命令行辅助工具。\ntldr # tldr - \u0026ldquo;too long, don\u0026rsquo;t read\u0026rdquo;，和 cheat 类似，列出某个命令的常见使用案例。它是一个社区驱动的项目。\nbottom # bottom - 运行于终端的跨平台系统监视器，可视化。\ngtop # gtop - 和 bottom 有点类似，系统监控面板。\nglances # glances - 可用于替代 top/htop，监控 GNU/Linux、BSD、Mac OS 和 windows 系统。\nhyperfine # hyerfine - 压测工具，可同时压测多个命令。\ngping # gping - ping 的终端可视化版本，体验不错，值得一试；\nprocs # procs - rust 编写的 ps 的替代版本。\ncurlie # curlie - http 客户端，号称是，兼具 curl 的强大与 httpie 的易用性。\nxh # xh - 兼顾 httpie 的易用性，同时注重高性能的 http 客户端。\ndog # dog - 依旧是 rust 实现的 DNS 分析工具 dig 的替代版本。\n","date":"2023-11-06","externalUrl":null,"permalink":"/posts/2023-11-07-high-productivity-shell-commands-part4/","section":"文章","summary":" 本篇文章是介绍 modern-unix 仓库剩余的 20 个命令的上篇，外加 1 比 modern-unix 中更易于使用的命令。\n","title":"我的终端环境：高效 shell 命令（四）之 20+1 个 modern-unix 命令","type":"posts"},{"content":"本文将介绍的 3 命令，用于提高 Web 开发人员们的日常工作效率。\n前言 # 对 Web 开发而言，除了基本的框架外，日常开发过程中，还常用的必然就是调试工具。本文将要介绍的三个命令分别是 entr、httpie、jq，变主要是为了这个目的而生的。\n大概说明，如下所示：\nentr，它的主要作用是在当监听文件变化后，执行相应的命令； httpie，相对于 curl，一款体验更加友好的 http client 命令； jq，一款强大的 JSON 数据的解析命令，甚至可简单的编程； 这三个命令在日常的 web 开发过程中扮演着不同的角色。\n开始具体介绍。\nentr 实现 Live Reloading # entry 命令的是什么？直接看演示效果，如下所示：\n大概看出，entr 是用于监听文件变化并执行指定命令。看到这，有没有想到啥？对，entr 可用于服务的热加载（Live Reloading）。\n安装 # 安装命令，如下所示：\nbrew install entr 案例 # 最直接的示例效果，监听文件变化并发出通知，如下所示：\nls test.txt | entr echo \u0026#34;test.txt changed!\u0026#34; OK, 现在想到啥了吗？这个命令对于 Web 开发人员有什么样的价值呢？\n回想一下，平时开发 web 程序时，我们是不是经常手动重启服务呢？\n如果不是，或许是你所用框架默认支持或是你通过其他工具实现了 live-reading，比如我所有的博客工具 hugo，就有内置了自动加载的能力。\n但由于这并不是框架默认能力，不同语言不同框架都要去寻找相应的实现方案。\n庆幸的是，通过 entr，可以很方便地实现这个功能。\n以一个 Go server 为例，文件名是 main.go，代码如下所示：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte(\u0026#34;Hello World!\u0026#34;)) }) fmt.Println(\u0026#34;Server is listening on :3000\u0026#34;) http.ListenAndServe(\u0026#34;:3000\u0026#34;, nil) } 通过如下命令，fd 实现：\nfd -e go | entr -r go run *.go 重启服务 效果如下：\n另外，如希望每次重新启动后，执行清屏操作清屏，可使用 -c 选项。效果如下所示：\n但是，这里还有一个缺点，检查有新文件创建，entry 默认无法检测。\n针对这个问题，entry 提供了一个选项 -d，它在检测到新文件后，会重新执行 entr 命令。由于是停止重新执行，要通过使用 while 循环执行。\n完整命令，如下所示：\nwhile true do fd *.go | entr -d -r go run *.go; done 看起来已经足够使用是吗？但其实，这里依然有个大问题，那就是 while 如果无法退出。即使是强制 kill 掉 go run 进程，依然重新启动。\n引入一个命令 - trap，协助解决。它的主要作用是捕捉指定信号就会执行相应命令。\ntrap \u0026#34;echo \u0026#39;command you want to execute\u0026#39;\u0026#34; SIGINT; while true do sleep 10; done 捕捉 SIGINT 信号，并打印 echo \u0026quot;command you want to execute\u0026quot;。\n注：这个命令建议不要尝试，不然只要杀死终端才能退出。\n效果如下所示：\n如此的话，简单改造下前面的命令。\n现在，在源码根目录下创建一个名为 run.sh 的文件，源码如下所示：\n#!/bin/bash trap \u0026#34;exit;\u0026#34; SIGINT; while true; do fd -e go | entr -rcd go run *.go; done 查看演示效果，如下所示：\n到这里，就为我们的项目添加了一个实时构建编译的能力。\n虽说，不同语言可能都有自己的管理工具，现在有了 entr 这个命令，就能以最简单的方式实现我们的需求。\nhttpie 人性化 HTTP Client # httpie 是一款更人性化的 HTTP 命令行客户端，简单来说，比 curl 更加易于使用。\n安装 # brew install httpie 案例 # 最简单的使用案例，快速发起 GET 和 POST 请求。\nGET 请求：\nhttp GET http://httpbin.org/get name==poloxue age==18 POST 请求：\nhttp POST http://httpbin.org/post name=poloxue age=18 http 命令的完整语法，如下所示：\nhttp [flags] [METHOD] URL [ITEM [ITEM]] 一般使用 # 从易用性角度，如果拷贝一个 url 可直接通过在 scheme 之后添加一个 \u0026lt;space\u0026gt; 便可直接使用：\n例如，GET 方法请求 http://httpbin.org/get，如下所示：\nhttp ://httpbin.org/get 演示效果：\n另外，METHOD 默认也可省略的，省略规则是：\n当有 data：POST，如 http ://httpbin.org/post name=poloxue age=18 当无 data：GET，如 http ://httpbin.org/get httpie 还支持一种 offline 模式，debug 神器，只打印 HTTP 请求文本，但不进行网络请求；\nhttp --offline ://httpbin.org/get 输出内容：\nGET /get HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Connection: keep-alive Host: httpbin.org User-Agent: HTTPie/3.2.2 现在，通过 offline 快速了解 httpie 的 header 设置，query 参数、JSON 请求体 等；\n几个规则，如下所示：\nheader：使用 key:value 设置 header； query string，使用 key==value，表示 query params，快速拼接 query string； body json data，使用 key=value 即可，更复杂结构，可通过命令管道或文件导入； 一个命令演示下如何设置：\nhttp --offline POST http://httpbin.org/post \\ X-API-Token:123456\\ User-Agent:foo\\ name==poloxue\\ age==18\\ password=123456\\ email=poloxue123@gmail.com 启用 offline 调试模式，输出结果：\nPOST /post?name=poloxue\u0026amp;age=18 HTTP/1.1 Accept: application/json, */*;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Content-Length: 55 Content-Type: application/json Host: httpbin.org User-Agent: foo X-API-Token: 123456 { \u0026#34;email\u0026#34;: \u0026#34;poloxue123@gmail.com\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;123456\u0026#34; } 输出美化 # httpie 还有一个能力，它默认会对返回数据进行格式化与语法高亮处理。如此一来，其他的 json 格式化命令就用不到了，如早前介绍的 pp_json 这个命令。毕竟，httpie 的格式美化能力更强大。\n想修改它的配色风格？\n通过 --style 选项即可修改。\nhttp --style autumn GET ://httpbin.org/get 如果想查看所有可用该配色，直接输入 http --style 即可查看：\n输出内容如下：\nusage: http -s/--style {abap, algol, algol_nu, arduino, auto, autumn, borland, bw, colorful, default, dracula, emacs, friendly, friendly_grayscale, fruity, github-dark, gruvbox-dark, gruvbox-light, igor, inkpot, lightbulb, lilypond, lovelace, manni, material, monokai, murphy, native, nord, nord-darker, one-dark, paraiso-dark, paraiso-light, pastie, perldoc, pie, pie-dark, pie-light, rainbow_dash, rrt, sas, solarized, solarized-dark, solarized-light, staroffice, stata, stata-dark, stata-light, tango, trac, vim, vs, xcode, zenburn} [METHOD] URL [REQUEST_ITEM ...] error: argument --style/-s: expected one argument for more information: run \u0026#39;http --help\u0026#39; or visit https://httpie.io/docs/cli httpie 的格式方式也是可配置的，选项 --pretty，可配置值有 all|colors|format|none，看名字大概能猜出它们的区别：\nall，默认值，即高亮又格式化； format，不高亮但格式化； colors，高亮但不格式化； http --style=autumn ://httpbin.org/get 演示效果：\n文件下载 # 通过 --donwload 即可：\nhttp --download https://github.com/httpie/cli/archive/master.tar.gz 文件下载，除去最基本的需求，肯定还是要支持断点续传的。\n要支持断点续传的话，首先，要通过 --output/-o 选项指定输出的文件名称。\nhttp --donwload -o httpie.tar.gz https://github.com/httpie/cli/archive/master.tar.gz 断点续传，要启用 --continue/-c 选项。\n为了掩饰效果，选择一个大文件进行下载，下载飞书的海外版 Mac 安装包。\n命令如下：\nhttp -dco lark.dmg https://sf16-va.larksuitecdn.com/obj/lark-artifact-storage/49f5b75a/Lark-darwin_x64-6.11.16-signed.dmg 演示效果，如下所示：\nhttpie 就介绍这么多，其他还有 cookie、session，authentication 等等请自行查阅文档 httpie 文档。\njq - 强大的 JSON 处理器 # jq，一款可用于处理解析 JSON 文本的命令，它非常强大，甚至是可编程的。\n安装 # brew install jq 案例 # 最简单的使用场景，JSON 文本格式化，命令如下所示：\ncurl https://coderwall.com/bobwilliams.json | jq \u0026#39;.\u0026#39; 输出内容如下：\n{ \u0026#34;id\u0026#34;: 26098, \u0026#34;username\u0026#34;: \u0026#34;bobwilliams\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Bob Williams\u0026#34;, \u0026#34;location\u0026#34;: \u0026#34;Charleston, SC\u0026#34;, \u0026#34;karma\u0026#34;: 1010, \u0026#34;accounts\u0026#34;: { \u0026#34;github\u0026#34;: \u0026#34;bobwilliams\u0026#34;, \u0026#34;twitter\u0026#34;: \u0026#34;_bobwilliams\u0026#34; }, \u0026#34;about\u0026#34;: \u0026#34;hacker, lifter, drummer and happily married, proud father of three\\n\\n\\n[LinkedIn](http://www.linkedin.com/pub/bob-williams/17/a6a/314)\\n\\n\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;CTO\u0026#34;, \u0026#34;company\u0026#34;: \u0026#34;SPARC\u0026#34;, \u0026#34;team\u0026#34;: 4521, \u0026#34;thumbnail\u0026#34;: \u0026#34;https://coderwall-assets-0.s3.amazonaws.com/uploads/user/avatar/26098/photo.JPG\u0026#34;, \u0026#34;endorsements\u0026#34;: 1010, \u0026#34;specialities\u0026#34;: [], \u0026#34;badges\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;Platypus\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Have at least one original repo where scala is the dominant language\u0026#34;, \u0026#34;created_at\u0026#34;: \u0026#34;2014-04-22T09:02:31.443Z\u0026#34;, \u0026#34;badge\u0026#34;: \u0026#34;https://dj1symwmxvldi.cloudfront.net/assets/badges/platypus-edcb8d16952f9cb27e9a1644d7d38b5606ff8f0c8a55869c4e0d8c42ed4ea637.png\u0026#34; }, { ... }, { ... }, { ... }, { ... }, { ... }, ] } 如果是通过 httpie 发起请求，jq 的这个能力就显的有点鸡肋了，虽说文本来源不一定都是 HTTP，但它还是占了大头不是。\njq 的强大之处在于，它对 JSON 随心所欲的处理能力，如 object 字段访问，数组访问，重新构造对象与数组，甚至是支持统计、排序、过滤等，基于原始 JSON 生成一份更有价值的数据。\n以一个需求为例，假设我希望将以上 JSON 进行一系列处理，得到如下的结果。\n{ \u0026#34;name\u0026#34;: \u0026#34;xxx\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;xxx\u0026#34;, \u0026#34;company\u0026#34;: \u0026#34;xxx\u0026#34;, \u0026#34;badges\u0026#34;: [\u0026#34;name01\u0026#34;, \u0026#34;name02\u0026#34;, \u0026#34;...\u0026#34;], \u0026#34;badge_count\u0026#34;: 0 } 注：其中，badges 只要求取出按 created_at 排序最新的 5 个，导出它们的 name。\n通过如下命令将 json 响应内容保存到一个 JSON 文件：\nhttps -o bobwilliams.json https://coderwall.com/bobwilliams.json 如何导出部分字段？\njq \u0026#39;{name: .name, title: .title, company: .company}\u0026#39; bobwilliams.json 输出如下：\n{ \u0026#34;name\u0026#34;: \u0026#34;Bob Williams\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;CTO\u0026#34;, \u0026#34;company\u0026#34;: \u0026#34;SPARC\u0026#34; } 如何导出 badges 最新的 5 个 badges:\njq \u0026#39;.badges | sort_by(.created_at) | reverse | [.[:5][].name]\u0026#39; bobwilliams.json 通过 sort_by 按 created_at 升序排列并通过 reverse 转为降序，使用 .[:index] 的语法进行切片，通过 [] 重新构造数组。\n输出如下：\n[ \u0026#34;Platypus\u0026#34;, \u0026#34;Python\u0026#34;, \u0026#34;Narwhal\u0026#34;, \u0026#34;Forked\u0026#34;, \u0026#34;Charity\u0026#34; ] 那么，还剩最后一个问题，只展示了最新的 5 个 badges，本身一共有几个徽章呢？\njq \u0026#39;.badges | length\u0026#39; bobwilliams.json 输出如下：\n11 最后，将以上各个部分进行重组，整合成一个新的 object 即可，完整表达式：\njq \u0026#39;{name, title, company, badges: [.badges | sort_by(.created_at) | reverse | .[:5][].name], badge_lenght: .badges | length}\u0026#39; bobwilliams.json 输出如下所示：\n{ \u0026#34;name\u0026#34;: \u0026#34;Bob Williams\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;CTO\u0026#34;, \u0026#34;company\u0026#34;: \u0026#34;SPARC\u0026#34;, \u0026#34;badges\u0026#34;: [ \u0026#34;Platypus\u0026#34;, \u0026#34;Python\u0026#34;, \u0026#34;Narwhal\u0026#34;, \u0026#34;Forked\u0026#34;, \u0026#34;Charity\u0026#34; ], \u0026#34;badge_lenght\u0026#34;: 11 } jq 命令就是这么强大，如果平时在 Web 开发时，遇到大 JSON 输出，可通过 jq 帮助进行更加细致的格式化。\n总结 # 本文介绍了三个开发阶段常用的命令，分别是用于实时更新重启的命令 entr，更人性化的 http 客户端 httpie 和一款强大的 JSON 数据处理命令 jq。\n让它们提效我们的日常工作吧！\n","date":"2023-11-02","externalUrl":null,"permalink":"/posts/2023-11-02-high-productivity-shell-commands-part3/","section":"文章","summary":"本文将介绍的 3 命令，用于提高 Web 开发人员们的日常工作效率。\n前言 # 对 Web 开发而言，除了基本的框架外，日常开发过程中，还常用的必然就是调试工具。本文将要介绍的三个命令分别是 entr、httpie、jq，变主要是为了这个目的而生的。\n","title":"提效开发调试的三个命令","type":"posts"},{"content":"本文将介绍三个高效搜索命令，分别是 fd、ripgrep 与 fzf。\n我将介绍的 fd 和 ripgreap 对标的是传统 grep 和 find，它们在性能和使用体验上都有大幅提升。如果你成为 10x 程序员，强烈推荐使用它们。\nfd，目录与文件搜索命令，比默认 find 更易于使用，而且查找速度上更快；\nripgrep，可用于高效的内容搜索，比默认的 grep 命令速度更快；\nfzf，命令行交互式模糊搜索工具，可与其他命令进行结合，提高使用体验；\n如上的 fd 与 ripgrep 是由 rust 编写，性能上完虐传统的 find 与 grep。\n视频版本，没有文章详细。\nfd # fd 是一款文件查找命令，可替换系统默认 find，它的体验更友好，且查询效率极高。我们在使用传统的 find 时，要经常查手册看帮助文档，但使用 fd，它的默认行为就能满足我们大部分的需求。\n如何使用呢？\n安装 # brew install fd 递归 # 文件系统下搜索文件名，最常见的场景是递归搜索文件名包含 pattern 的文件，不知道有你是否能立刻想起来 find 如何写呢？\n示例：遍历查找。\n$ fd pattern 正则 # 如果要正则查询，pattern 默认即支持正则表达式。\n查找文件名包含日期的文件：\n或者查找所有的 go 代码文件。\n这个表达式更加正确的表述是 fd .*\\.go$，查出所有以 .go 结尾的文件。\n通配符 # fd 同样是支持统配符的，通过 -g 选项指定通配符。\n文件类型 # 前面的查找 go 文件，其实也是查找类型是 Go 源码。但正则和通配符，都比较繁琐，可直接通过 fd -e go pattern 的模式直接寻找指定扩展的文件。\n示例：查找 python 源码文件。\n如果无需 patern，则表明查找所有的 .py 的文件。\n隐藏文件和 .gitinogre # fd 搜索效率高的一个原因是它默认不查找隐藏文件和 gitignore 文件。\n如想启用隐藏文件的查找，可指定 -H 选项，示例如下：\n从上图可知，其中包含了隐藏目录 .git 下的文件。\n除了默认忽略 hidden 文件，fd 还会忽略 .gitignore 中的文件，选项 -I 即可查找包含在 .gitinogre/.fdignore/.ignore 的文件。\n$ fd -I pattern 其他示例 # fd 的更多选项，我就不一一介绍了，可以查看如下表格。\n案例 命令 说明 忽略大小写 fd -i readme.md 包含 readme.md 和 README.md 大小写的 smartcase 模式 fd -s readme.md 包含 readme.md 和 README.md \u0026mdash; fd -s README.md 只包含 README.md 查找结果显示详细信息 fd -l .go 查询结果显示文件详情 大小过滤 fd -S +1000k 查询大小 \u0026gt; 1000k 的文件 仅查找目录 fd \u0026ndash;type/-t directory golang 查询所有目录文件 查询并执行命令 fd \u0026ndash;type/-t file -x wc -l 查询文件并计算行数 \u0026mdash; fd \u0026ndash;type/-t file -X vim 查询所有文件并用 vim 打开 性能 # 除了忽略隐藏和 gitignore 中的文件外，fd 在性能上也是碾压 find。通过 hyperfine 命令测试 fd 与 find。\nhyperfine --warmup 3 \u0026#34;find . -iname *.go\u0026#34; \u0026#34;fd -i -g \u0026#39;*.go\u0026#39; -uu\u0026#34; 性能对比结果：\n压测结果显示，fd 相较于 find 快了 3.88 倍。\nripgrep # ripgrep 是一款文本搜索命令，功能与 grep 类似。和 fd 之于 find 一样，ripgrep 在体验和性能上同样完胜 grep。\n安装 # brew install ripgrep 递归 # ripgrep 的默认行为也是递归搜索，命令为 rg pattern，且默认高亮显示，与 grep --color main . -nR 对比，明显更加简洁易用，体验更好。\n示例效果：\n指定目录 # ripgrep 最后的参数即可指定搜索的目录。\nrg main ~/Code/golang-examples/ 指定文件 # 搜索指定文件与指定目录类似，命令的最后一个参数指定即可。\nrg main ~/Code/golang-examples/main.go 通配符 # 我们使用 -g 通过通配符指定搜索路径。\n如下是禁用目录递归：\nrg main -g \u0026#39;!/*/*\u0026#39; 还可实现如排除指定的文件：\nrg main -g \u0026#39;!main.go\u0026#39; 或者排除指定的目录\nrg -g \u0026#39;!directory\u0026#39; 正则 # ripgrep 支持通过 -e 选项启用正则表达式搜索，如搜索文件中的指定日期格式内容。\nrg -e \u0026#39;[0-9]{2}:[0-9]{2}\u0026#39; 默认过滤 # 和 fd 一样，ripgrep 的高效率搜索能力一方面也是因为默认忽略了一些文件，如它忽略隐藏文件以及 .gitgnore .ignore .rgignore 中的文件。禁用 ignore 可通过选项 \u0026ndash;no-ignore 即可。\n对于隐藏文件，通过 --hidden 即可搜索包含隐藏文件。\n如果希望彻底禁用隐藏能力，通过 -uuu 实现，如 rg -uuu pattern。\n大小写敏感 # ripgrep 默认即大小写敏感。\nrg pattern 通过 -i 选项可忽略大小写。\nrg -i pattern 或者通过 -S 启用 smartcase 模式，即\nrg -S pattern 搜索并替换 # ripgrep 中，我们通过 -r 可直接替换搜索结果，但不改变原内容。\nrg main ~/Code/golang-examples r main # 只替换输出，未修改文件 配置 # ripgrep 支撑配置文件修改 ripgrep 的默认行为。我们通过设置环境变量 RIPGREP_CONFIG_PATH 指定配置文件路径。\n配置文件的配置方式和上文介绍的 bat 类似，都是通过 --xxx 选项的形式设置。\n# Don\u0026#39;t let ripgrep vomit really long lines to my terminal, and show a preview. --max-columns=150 --max-columns-preview # Add my \u0026#39;web\u0026#39; type. --type-add web:*.{html,css,js}* # Search hidden files / directories (e.g. dotfiles) by default --hidden # Using glob patterns to include/exclude files or folders --glob=!.git/* # or --glob !.git/* # Set the colors. --colors=line:none --colors=line:style:bold # Because who cares about case!? --smart-case 如我们希望 ripgrep 默认启用 smartcase 能力，可将 --smart-case 直接配置到配置文件中。\nfzf # fzf 全名 fuzzy finder，一款通用的命令行模糊查找工具。它可与其他命令结合，提升其他命令的使用体验。\n安装 # brew install fzf 目录搜索 # fzf 的默认行为是在当前目录搜索。当然它的搜索和 fd 的搜索不同，它会进入交互式模式搜索文件或目录路径。\n它支持通过 CTRL+P/N 上下选择，确认搜索结果后，输入 Enter 确认后，它会将输出直接作为输出打印到标准输出。由于它输出为标准输出，我们就有了更多可能性，通过管道将它与其他命令结合。\n命令组合 # fzf 最大的魅力在于，我们可将其与其他命令组合，如\n将其他命令的输出作为 fzf 的输入，基于它进行搜索； 或将 fzf 的搜索内容作为其他命令的输入，更智能使用其他命令。 我们具体介绍下吧。\n首先，我们示例将 one、two、three、four 作为输入，通过 fzf 搜索选择。\necho \u0026#34;one\\ntwo\\nthree\\nfour\u0026#34; | fzf 更多可能就诞生了，我们可以讲 ls 的输出作为 fzf 的输入。\nls | fzf 将 fd 查找结果作为 fzf 的输入。\nfd --type file | fzf 接下来，我们尝试将搜索结果作为其他命令的输入。毕竟，如果 fzf 的搜索结果只是输出到终端，那就太可惜了，可将其作为其它命令的输入。\n如将 fzf 搜索结果作为 vim 的输入，助力 vim 快速打开文件。\nvim `fzf` 我们也可以将多个命令组合，fzf 从其他命令接收输入，同时将搜索结果输出给其他啊命令。这样就能实现更多可能。\n是不是已经发现，fzf 实现更多可能性的能力，如 command1 | fzf | xargs command2，将 command1 的结果作为 fzf 的搜索来源，将 fzf 的确认结果作为 command2 的输入。\n如上文中的 zoxide （高效 cdi 命令）快速进入文件能力，我们可用 fzf 实现：\ncd `zoxide query --list {querystring} | fzf` 默认的 zoxide query --list 只记录的是 zoxide 中的历史记录，如想实现进入任意的目录，可使用如下命令：\ncd `fd --type=directory | fzf` 如下是一个 cdg 命令，作用是交互式搜索全局任意目录并通过 cd 进入。可 zoxide 中的 cdi 对比。\nalias cdg=\u0026#39;cd_global() {cd $(fd --type directory $1 $2 | fzf)}; cd_global\u0026#39; 使用方式形如 cdg pattern directory，在哪一个目录下查询包含 pattern 的目录，确认后即可 cd 进入到这个目录。\n总结 # 本文介绍了三个实现终端高效搜索的命令，分别是用于文件查找的 fd、文本搜索的 ripgrep 及交互式搜索工具 fzf。希望通过将这几个命令的组合使用，再进一步提升我们的终端使用效率。\n希望本文对你有所帮助，感谢阅读。\n","date":"2023-10-30","externalUrl":null,"permalink":"/posts/2023-10-30-high-productivity-shell-commands-part2/","section":"文章","summary":"本文将介绍三个高效搜索命令，分别是 fd、ripgrep 与 fzf。\n我将介绍的 fd 和 ripgreap 对标的是传统 grep 和 find，它们在性能和使用体验上都有大幅提升。如果你成为 10x 程序员，强烈推荐使用它们。\n","title":"推荐 3 个高效搜索命令","type":"posts"},{"content":"如下是视频版本，没有文章详细。\n类 Unix 系统发展多年，不少古董命令还在占据终端的绝大部分时间，但它们的使用体验上却是差强人意。最能说明问题的就是那个 cd 命令，无论是多么丝滑的操作，一旦遇到需要 change directory 就会变得磕磕绊绊。\n从本文开始，我计划用几篇文章介绍提升终端效率的一系列命令，它们更具现代风格，希望能让你眼前一亮。\n前言 # 本文是高效命令系列第一篇，将先介绍平时工作中最常用的与目录文件相关的命令，分别是替换 ls 的 exa，替换 cd 的 zoxide 和替换 cat 的 bat。它们的优势会在文章中逐步展开说明。\n特别提醒：exa 已停止维护，可用 exa 的 fork 版本 eza 替代。\n正式开始前，先推荐一个 github 仓库 - modern-unix，其中收录了大量的更具现代风格的命令，可用于替换一大波老古董命令，在这个系列的最后一篇，我将会整体过下该仓库中的所有命令。希望通过这些命令的学习，能进一步提升我们的效率。\nexa # 首先是 exa，它是一款可用于替换系统默认 ls 的命令，在平时工作中 ls 几乎使用最多的命令，而 exa 在支持 ls 的基本能力基础上，提供了更丰富的特性。\n快速安装 # brew install exa # 其他系统请查看 GithHub README.md 使用 # 首先，exa 默认提供了配色效果，无需 ls 要追加 --color 参数，省去了 alias 别名的设置。\n其次，exa 支持显示文件图片，通过指明 --icons 实现，显示文件类型图标；\n更复杂的命令，支持 ls -l 显示文件列表详情，添加头部说明 header，如果是 git 仓库，可显示文件的 Git 信息。\n如果想显示文件数，也不需要单独安装 tree 命令，如下 exa --tree --icons，显示文件树；\n别名 # 如果你钟爱 exa 的这些能力，可通过别名将默认 ls 和 tree 命令替换为 exa。\n如下是一些常见的别名设置：\n# 默认显示 icons： alias ls=\u0026#34;exa --icons\u0026#34; # 显示文件目录详情 alias ll=\u0026#34;exa --icons --long --header\u0026#34; # 显示全部文件目录，包括隐藏文件 alias la=\u0026#34;exa --icons --long --header --all\u0026#34; # 显示详情的同时，附带 git 状态信息 alias lg=\u0026#34;exa --icons --long --header --all --git\u0026#34; # 替换 tree 命令 alias tree=\u0026#34;exa --tree --icons\u0026#34; 如此一来，就将 exa 设置为系统默认 ls。\n除了 exa，还有两个可用于可替代 ls 的命令，分别名为 colorls 和 lsd，colorls 的性能太差，如果是目录中的文件数量多，能感觉到明显的卡顿。lsd 的号称是下一代的 ls，使用起来没有 colorls 那么卡，但性能上也不如 exa。\n如果你对它们感兴趣，可以研究下。\n特别提醒，别名生效后，如果要用原始命令，要通过类似于 \\command 的形式实现，如 \\ls 将无效别名设置，直接使用系统内置 ls 命令。另外，如果你的 shell 脚本里使用了它，可能影响这些脚本的执行。这是设置别名替换系统命令要考虑的问题。\n我们继续介绍下一个命令 - zoxide。\nzoxide # 在正式介绍 zoxide 前，尝试提前问自己一个问题，Linux 默认命令 cd 好不好用？我的答案是，相当难用，无论多么丝滑的操作，一旦遇到 cd，只能说一句 f**k。\n你是不是经常这样使用 cd 呢？\ncd ../ cd ../ cd ../ cd ../../../ cd x/ cd y/ cd z/ 如果不想被 cd 折磨的话，我强烈推荐这个工具：zoxide。\nzoxide 是一款受到 z 和 autojump 启发而来的命令，它会记录访问过的目录，通过搜索找到最匹配你目标的目标。从而实现以 最最最最 少按键就能实现目录跳转。\n一般情况下，我们关注的目录就那几个，90% 的情况用它的快速跳转能力即可，而一些特殊情况，cd 绝对路径即可，亦或者是使用它提供的另一种方式，交互式搜索。\n前面我写过一篇文章 介绍了 oh-my-zsh 提供的 z 插件，zoxide 与 z 相比更易于使用。这有一份对比报告：zoxide vs zsh-z。\n具体介绍它的安装使用吧。\n安装 # brew install zoxide # 其他系统请查看 GithHub README.md 配置 # 在 zsh 中使用 zoxide 要简单配置下，一行命令将 zoxide 初始化命令追加到 ~/.zshrc 中。\n如下所示：\necho \u0026#39;eval \u0026#34;$(zoxide init zsh --cmd z)\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.zshrc 生效后，即可通过 z 命令使用 zoxide 的能力。\n亦或者，如果觉得要从习惯于使用 cd 切换到 z 有难度，可直接将 z 命名为 cd，直接替换掉系统的 cd 命令，配置 --cmd cd 即可。\necho \u0026#39;eval \u0026#34;$(zoxide init zsh --cmd cd)\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.zshrc 我将会使用 zoxide 直接替换 cd 命令进行演示，即第二种配置方式。\n使用 # 正式开始使用前，先假设我有如下目录结构：\n~/Hello |_ ./golang-examples |_ ./python-examples |_ ./rust-examples |_ ./trading-strategies 由于 zoxide 是在历史访问路径的基础上智能选择。为了便于演示，我把之前的访问历史记录已经清空了，并通过如下命令初始化访问历史：\ncd ~/Hello/golang-examples cd ~/Hello/python-examples cd ~/Hello/rust-examples cd ~/Hello/trading-strategies 假设现在在 ~ 用户目录下，如下是演示效果：\n主要演示了 zoxide 的三个能力，分别是全名匹配、部分匹配和如果存在重名目录下会选择智能最优的目录。\n首先是全名匹配，通过 zoxide 的 cd 命令，在 ~ 目录下快速跳转到 golang-examples 目录，直接输入 cd golang-examples。\n其次是部分匹配，由于记录中只有一个目录包含 python，执行 cd python 会直接进入到 python-examples；\n最后是重名按算法选择最优目录，我不清楚它的具体算法，但观察来看，它当前在 python-examples 目录下，执行 cd examples 会优先找到最近访问的包含 examples 的目录，且非当前位置，即 golang-examples。\n如果觉得它的智能算法不够灵活，还可以尽量补全路径，和使用普通 cd 一样。如果还想要它的效率，也可以进入它的交互搜索模式，zoxide 支持两种方式进入交互模式：\n一种是输入 cd + 目标名称 + 快捷键 \u0026lt;Space\u0026gt;+\u0026lt;Tab\u0026gt;，进入交互选择模式，效果如下：\n另一种是直接使用 cdi 命令也可进入交互搜索模式，如下所示：\n补充，zoxide 本身也是个命令，你可以用它增删改查，管理历史方案记录。\n$ zoxide query golang # 返回最匹配 golang 的目录 $ zoxide query golang --list # 返回所有匹配 golang 的目录 $ zoxide -h # 更多帮助信息 zoxide 0.9.2 Ajeet D\u0026#39;Souza \u0026lt;98ajeet@gmail.com\u0026gt; https://github.com/ajeetdsouza/zoxide 一个更智能的终端 `cd` 命令 使用方法： zoxide \u0026lt;命令\u0026gt; 命令： add 添加一个新目录或增加其排名 edit 编辑数据库 import 从另一个应用导入条目 init 生成 shell 配置 query 在数据库中搜索目录 remove 从数据库中移除目录 差不多，zoxide 大概就介绍的这么多吧。希望对于想用它替换掉 cd 的朋友有所帮助吧。\nbat # 说完了 ls 列举目录，cd 进入目录，我们继续介绍一个命令，bat 查看文件内容。\n首先，bat 和 Baidu/Alibaba/Tencent 没有联系，它是一款支持语法高亮、GIT 集成的用于替换类 Unix 系统下快速查看文件内容的命令，功能与 cat 相似的命令。\n我们直接介绍它的安装与使用吧。\n安装 # brew install bat # 其他系统请查看 GithHub README.md 使用 # 对于 bat 命令，我先介绍它的使用，然后再谈配置，因为配置并非它的必选项而是优化项。\nbat 相比于 cat 的第一个优势，就是它支持语法高亮效果与行号显示。如我们查看一个 Go 的源码文件，效果如下：\n而且，bat 还集成 Git。如下我们修改了 logger.go 文件，通过 bat 即可查看它的修改点；\n默认情况下，bat 采用分页输出，这对于读取大文件非常有帮助，不用担心失误导致产生一大片控制台输出。但如果你希望 bat 和 cat 一样，一次性无分页输出文本，可通过 --pager=never 或 --no-pager 选项实现。\nbat --pager=never logger.go bat --no-pager logger.go 如果你习惯使用 cat 的模式，希望默认不启用分页能力，可直接在配置文件配置默认行为，在其中增加 --pager=never。\n接下来说说如何通过 bat 的配置改变它的默认行为吧。\n配置 # bat 的配置文件路径是通过环境变量指定的。我们在 .zshrc 中设置 bat 配置文件位置环境变量。\nexport BAT_CONFIG_PATH=\u0026#34;${XDG_CONFIG_HOME:-~/.config/bat.conf\u0026#34; 生效后，执行如下命令将会生成配置文件：\nbat --generate-config-file 生成配置文件，位于 ~/.config/bat.conf。\n假设我不喜欢 bat 默认的主题，就可以通过配置修改了。如配置 bat 默认选项，将主题改为 --theme=TwoDark 启用：\n# Specify desired highlighting theme (e.g. \u0026#34;TwoDark\u0026#34;). Run `bat --list-themes` # for a list of all available themes --theme=TwoDark 如果你想查看更多主题，可通过 bat --list-themes 查看 bat 支持的主题列表。\n现在，不想启用 bat 的分页能力，在配置中添加：\n--pager=never 别名 # 觉得 bat 不错，想直接替换 cat 命令，在 zshrc 中配置别名即可，将默认 cat 命令，替换为 bat，如下所示：\nalias cat=\u0026#39;bat\u0026#39; 总结 # 本文介绍了三个命令，分别是 exa(eza)、zoxide 和 bat 的使用，都是日常工作中最常用的三个命令。利用好它们，最常用最无聊的命令也能产生有趣的体验，实现效率提升。\n最后，希望你喜欢这篇文章，且真正给你带来了帮助。\n","date":"2023-10-27","externalUrl":null,"permalink":"/posts/2023-10-28-high-productivity-shell-commands-part1/","section":"文章","summary":"如下是视频版本，没有文章详细。\n类 Unix 系统发展多年，不少古董命令还在占据终端的绝大部分时间，但它们的使用体验上却是差强人意。最能说明问题的就是那个 cd 命令，无论是多么丝滑的操作，一旦遇到需要 change directory 就会变得磕磕绊绊。\n","title":"用 exa/zoxide/bat 替换 ls/cd/cat 命令","type":"posts"},{"content":"不知道你是否想过自定义 Shell 提示符主题能带来的不仅是终端美观度的提升，还能通过视觉优化增强了工作效率呢？\n在众多 shell 提示符主题中，Powerlevel10k 因为支持高度可定制和丰富的功能选，非常值得推荐。本文基于这个主题介绍 zsh 主题插件 powerlevel10k，包括它的安装和配置自定义。\n什么是 powerlevel10k? # Powerlevel10 是一款 zsh 的主题，强调性能、灵活性和开箱即用，但同时自定义能力极强。前面介绍 zsh 轻量级框架 oh-my-zsh 时，提到过一些 zsh 主题，而通过 p10k（powerlevel10k 的简称）的自定义配置化能力，同样能配置出覆盖出之前主题的类似效果，当然相对而言，也更加强大。\n效果展示：\n安装依赖字体 # 在安装 powerlevel10k 前，要先安装它依赖的字体：NerdFont。不同系统下的安装方法，查看它的文档。\n简单说下 Nerd Fonts 字体。它是一系列开源字体的集合，被特别增强，它包含大量的图标和符号，如开发工具、编程语言和版本控制系统的图标。这些字体对于提高我们终端和编辑器的视觉体验和功能性有着极大帮助。\n有了它，我们的终端才能显示一些复杂字体甚至是图标。\nMacOS 的话，可直接通过 Homebrew 快速安装：\nbrew tap homebrew/cask-fonts brew install font-hack-nerd-font 安装完成，配置终端字体，进入 iTerm2 Settings -\u0026gt; Profiles -\u0026gt; Text -\u0026gt; Font -\u0026gt; MesloLGS NF 即可。\n现在，我们终端就支持 NerdFont 字体了。\n如何测试？\n接下来安装 Powerlevel10k 时，它会提示我们检查字体是否正确安装。\n安装 powerlevel10k # 先通过如下的命令下载插件源码放到指定的位置。\ngit clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k 在 ~/.zshrc 配置启动 powerlevel10k 主题插件。\nZSH_THEME=\u0026#34;powerlevel10k/powerlevel10k\u0026#34; 现在我们只要重新终端或执行 source ~/.zshrc 生效配置就会进入到配置向导流程中了。\n配置 powerlevel10k # 终端将进入到 powerlevel10k 的配置向导后，我们通过回答问题来完成自定义的安装流程。这个流程有点繁琐，我将一步一步介绍，要跟着操作可能更有感觉吧。\n前置问题 # 首先，为了确认 NerdFont 字体安装成功，正式开始配置前，要先提出几个问题，识别图片是否如描述说的那样。我们回答后才能进入到主题的自定义。\n这些问题有诸如：\nDoes this look like a diamond (rotated square)? 这是看起来钻石吗？ Does this look like a lock? 这看起来是锁吗？ Does this look like an upwards arrow? 这看起来是向上箭头吗？ 等等。\n如果已经成功安装配置了字体，这些问题看起来就像逗傻子一样，按实际情况回答问题即可。如果字体安装成功，都是选择 \u0026ldquo;Yes\u0026rdquo; 即可。\n开始配置 # 进入到正式阶段，按步骤配置自己的提示符风格，下面一共 12 步，都是非常简单的回答问题。考虑到第一次配置，有懵逼的可能性，我把全部的步骤都列出来了。\n选择提示符风格，分别是 Lean、Classic、Rainbow 和 Pure。我选风格 3，大众所爱的风格，彩虹 Rainbow，则输入 3。\n字符集设置，毫无疑问，配置 unicode，选择 1。\n提示符显示当前时间风格。我选择 1，只显示命令耗时，不显示当前时间。\n提示符分隔符，也就是 src/master 之间的符号风格。我钟爱箭头，选择 1 -\u0026gt; Angled.\n提示符头部风格，就是 master \u0026gt; 的风格选择。毫无疑问，我钟爱箭头，选择 1 - Sharp。\n提示符尾部风格，钟爱箭头，但不是双箭头，选择 1 - Flat。\n提示符高度，显示一行还是两行，体验过两行，还是一行更紧凑一些，选择 1 - one line。\n两个命令键的间距，我喜欢两行离的近一点，选择 1 - Compact。\n提示符 Icons，多点 Icon 更帅，否则看起来就和一般主题没区别了，so 选择 2 - Many Icons。\n提示符丰富度，增加一些文本描述，帮助理解提示符中字符含义。还是简洁为美，毕竟空间不能占用太多，而且含义简单，无需文本辅助。我选择 1 - Consice。\n这是什么配置ne？提示符瞬闪？好像命令执行后提示符就立刻消失，只保留在最新的提示符，先选择 n - No 看看效果吧。\n提示符高性能模式，是否启用。推荐启用，就启用吧 1-verbose，如果发现有兼容问题，在重新配置 off。\n到此基本全部的配置都已经完成，powerlevel10k 命令行提示符的最终效果，如下所示：\n重新配置 # 在配置完成后，如果希望重新配置，重启整个流程，直接执行 p10k configure ，它会重新打开配置向导。\n配置文件 # 通过 powerlevel10k 的配置导航能快速自定义提示符的主题风格，但如想更细粒度的配置，可直接在 $HOME/.p10k.zsh 配置，配置导航只是最粗粒度的配置方式。\n如配置提示符两侧内容，通过 ~/.p10k.sh 中的变量 POWERLEVEL9K_LEFT_PROMPT_ELEMENTS 和 POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS 设置。\n配置文件的内容，如下所示：\ntypeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( # 操作系统图标 os_icon # 当前目录 dir # 版本控制信息 vcs # 提示符 # prompt_char ) typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( # 最后一条命令的退出码 status # 最后一条命令的执行时间 command_execution_time ... # 当前时间 # time ... ) 前面配置时，设置了不显示当前时间，可以通过打开 POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS 中的 time 重新显示时间。\n还有前面对提示符瞬闪的配置，transient 设置为 off。如果希望启用，同时有不重新经历一次配置向导，直接进入 ~/.p10k.zsh 配置变量 POWERLEVEL9K_TRANSIENT_PROMPT=always，即可。\n# Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt # when accepting a command line. Supported values: # # - off: Don\u0026#39;t change prompt when accepting a command line. # - always: Trim down prompt when accepting a command line. # - same-dir: Trim down prompt when accepting a command line unless this is the first command # typed after changing current working directory. typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always Powerlevel10k 还进一步集成了对各种工具的支持，包括但不限于 npm、k8s、Python 和 Go。在 ~/.p10k.zsh 中配置相应的提示元素，如 node_version、kubecontext、python_version 和 go_version。\n这些都是位于 POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS 配置项下。\ntypeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( # ... virtualenv anaconda pyenv goenv nodenv nvm nodeenv node_version go_version rust_version dotnet_version php_version laravel_version java_version package rbenv rvm fvm luaenv jenv plenv perlbrew phpenv scalaenv haskell_stack kubecontext terraform terraform_version aws aws_eb_env azure gcloud google_app_cred toolbox context nordvpn ranger nnn lf xplr vim_shell midnight_commander nix_shell chezmoi_shell vpn_ip load disk_usage ram swap todo timewarrior taskwarrior per_directory_history cpu_arch time newline ip public_ip proxy battery wifi example ) 这些配置让我们也可以在提示符中直观地看到当前环境的版本信息，以及 Kubernetes 上下文等关键信息，从而使我们的工作流程更加高效、直观。\n这里只是简单介绍了 powerlevel10k 的配置。想了解它的更多能力，可以在 ~/.p10k.sh 继续探索。\n总结 # 本文介绍了 powerlevel10k 的安装与配置。如果希望你命令行提示能给你提供更多信息，减少使用终端时的一些心智负担，如少执行几次版本，分支或所在上下文查询的命令，强烈推荐安装 powerlevel10k！\n","date":"2023-10-20","externalUrl":null,"permalink":"/posts/2023-10-20-zsh-theme-powerlevel10k/","section":"文章","summary":"不知道你是否想过自定义 Shell 提示符主题能带来的不仅是终端美观度的提升，还能通过视觉优化增强了工作效率呢？\n在众多 shell 提示符主题中，Powerlevel10k 因为支持高度可定制和丰富的功能选，非常值得推荐。本文基于这个主题介绍 zsh 主题插件 powerlevel10k，包括它的安装和配置自定义。\n","title":"终端环境：zsh 主题自定义 powerlevel10k","type":"posts"},{"content":"本文是高效终端环境第三篇，介绍 6 个可用于提效 zsh 效率的插件。系列查看：我的终端环境\n视频教程：\n今天，将会在 上文 的基础上，再介绍六个插件，其中 4 个是 oh-my-zsh 的内置插件，还有两个第三方插件。\n快速一览 # 本文将会涉及的插件，如下所示：\ncopypath，拷贝路径； copyfile，拷贝文件内容； copybuffer，拷贝命令行内容； sudo，快捷 sudo，命令行快捷添加 sudo 插件； zsh-history-substring-search，命令历史记录子字符串匹配； zsh-you-should-use，用于命令行 alias 别名提醒； 让我们正式开始。\n推荐一个网站 # 在开始前，我想先推荐一个 github 仓库，awesome-zsh-plugins，通过浏览器打开 awesome-zsh-plugins，里面提供了相当丰富的 zsh 的框架、教程、插件与主题等等，是 zsh 的资源合集。\n框架，如 oh-my-zsh，还有其他的一些框架。其中，还有关于 zsh 的教程。\n插件，上个视频介绍过的两个插件，zsh-syntax-highlighting - 命令行语法高亮插件, zsh-autosugggestions - 命令行自动建议提示插件，在这个文档里面都能找到。\n主题，除了 oh-my-zsh 内置主题，还有更多主题可选，如将在后面讲介绍的 powerlevel10k 这个 zsh 主题，在这个文档里也能找到。\n推荐插件 # 先说 oh-my-zsh 的内置插件。\n打开 zsh 配置文件 ~/.zshrc，将要使用的 oh-my-zsh 的内置插件提前配置。\nplugins=(... copypath copyfile copybuffer sudo ...) 保存退出，执行 source ~/.zshrc 生效。\ncopypath # copypath 的用途如其名，就是用来 copy 路径的。\n支持两种用法。\ncopypath: 无参数，直接拷贝当前路径；\ncopypath \u0026lt;文件或目录\u0026gt;：拷贝指定文件或目录的绝对路径；\n相比于 pwd 之后再拷贝，这种方式真的是省心省力的方式。\ncopyfile # copyfile，用于拷贝文件内容，命令格式 copyfile \u0026lt;文件路径\u0026gt;。\n假设，现有一个文件 test.txt。\ncat test.txt Hello oh my zsh 一个测试命令，copyfile test.txt，即可将 test.txt 文件中的内容拷贝到剪贴板中。\n效果如下：\n无需鼠标选中复制粘贴。\ncopybuffer # copybuffer，是用于快速复制当前命令行的输入。\n如何使用呢？\n它不同于前面两个快捷键，要通过 CTRL+o 快捷键拷贝命令行内容。\n特别说明，我在测试的时候，发现 copybuffer 与 vi-mode 存在冲突，不过如果启用了 vi-mode， 命令行内容拷贝可直接使用 yy，无续开启 copybuffer；\nsudo # sudo 的主要作用是，当我们输入某个命令，如 vim /etc/zshrc，发现没有系统权限，利用 sudo 插件，可快速将 sudo 作为前缀添加到命令最前面。\n演示效果如下所示：\n其他插件 # 介绍完 oh-my-zsh 的内置插件，继续介绍两个三方插件，分别是 zsh-history-substring-search 和 you-should-use.\n将 zsh-history-substring-search 和 zsh-you-should-use 两个插件下载配置。\ngit clone https://github.com/zsh-users/zsh-history-substring-search ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-history-substring-search git clone https://github.com/MichaelAquilina/zsh-you-should-use.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/you-should-use 打开 ~/.zshrc 文件，更新如下内容：\nplugins=(... zsh-history-substring-search you-should-use) history-substring-search # 先介绍 zsh-history-substring-search。它的主要用途是什么？\n一般情况下，在使用 zsh 时，通过 ↑ 或 ↓ 方向键，能实现类似按前缀匹配补齐的效果。\n而如果输入的是中间的字符串，则没法自动补齐。这个插件真是为这个目的而生的。\n使用这个插件前，除了启用插件以外，还需要进一步配置下，将 zsh-history-substring-search 提供的能力绑定到快捷按键。\n例如，上下方向键 ↑ 和 ↓。\nbindkey \u0026#39;^[[A\u0026#39; history-substring-search-up bindkey \u0026#39;^[[B\u0026#39; history-substring-search-down 在生效配置后，测试失败的话，查看文档，其中有介绍：\nHowever, if the observed values don\u0026rsquo;t work, you can try using terminfo:\nbindkey \u0026ldquo;$terminfo[kcuu1]\u0026rdquo; history-substring-search-up bindkey \u0026ldquo;$terminfo[kcud1]\u0026rdquo; history-substring-search-down\n那我们就增加这两行配置吧。\nbindkey \u0026#34;$terminfo[kcuu1]\u0026#34; history-substring-search-up bindkey \u0026#34;$terminfo[kcud1]\u0026#34; history-substring-search-down 除了 ↑ ↓ 按键外，我一般还习惯使用 CTRL+P/N 上下查找历史记录，配置如下：\nbindkey \u0026#39;^p\u0026#39; history-substring-search-up bindkey \u0026#39;^n\u0026#39; history-substring-search-down 如果希望支持 vi 的 jk，配置如下：\nbindkey -M vicmd \u0026#39;k\u0026#39; history-substring-search-up bindkey -M vicmd \u0026#39;j\u0026#39; history-substring-search-up 保存生效配置，测试下最终的成功成果吧。效果如下所示：\n另外，高亮样色可配置化的，可通过类似如下语法实现：\nexport HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND=(bg=none,fg=magenta,bold) 设置 background 为 none，即无色，而 front 设置为 magenta,bold。效果如下：\n如上的 zsh 的颜色变量，可查看 zsh 仓库文档 发现更多颜色。\ncolor=( # Codes listed in this array are from ECMA-48, Section 8.3.117, p. 61. # Those that are commented out are not widely supported or aren\u0026#39;t closely # enough related to color manipulation, but are included for completeness. # Attribute codes: 00 none # 20 gothic 01 bold # 21 double-underline 02 faint 22 normal 03 italic 23 no-italic # no-gothic 04 underline 24 no-underline 05 blink 25 no-blink # 06 fast-blink # 26 proportional 07 reverse 27 no-reverse # 07 standout 27 no-standout 08 conceal 28 no-conceal # 09 strikethrough # 29 no-strikethrough # ... # Bright color codes (xterm extension) 90 bright-gray 100 bg-bright-gray 91 bright-red 101 bg-bright-red 92 bright-green 102 bg-bright-green 93 bright-yellow 103 bg-bright-yellow 94 bright-blue 104 bg-bright-blue 95 bright-magenta 105 bg-bright-magenta 96 bright-cyan 106 bg-bright-cyan 97 bright-white 107 bg-bright-white ) you-should-use # you-should-use 用途是，如果执行的命令存在别名，会自动提示推荐使用的别名；\n由于，默认的提示信息在命令输出之前，添加如下配置：\nexport YSU_MESSAGE_POSITION=\u0026#34;after\u0026#34; 它的作用是，实现将提示信息打印在命令输出的最后。\n最终效果演示，如下：\n总结 # 本文介绍了 6 个 zsh 插件，每个插件都有特定的场景用途，希望能给大家的日常工作提升效率。\n","date":"2023-10-18","externalUrl":null,"permalink":"/posts/2023-10-19-zsh-6-powerful-plugins/","section":"文章","summary":"本文是高效终端环境第三篇，介绍 6 个可用于提效 zsh 效率的插件。系列查看：我的终端环境\n视频教程：\n今天，将会在 上文 的基础上，再介绍六个插件，其中 4 个是 oh-my-zsh 的内置插件，还有两个第三方插件。\n","title":"终端环境：6 个强大的 zsh 提效插件","type":"posts"},{"content":"前文中，对 iTerm2 已经有了一个大概认识。但一个高效的终端环境，离不开一个优秀 shell 解释器。\n视频版本，没有文章详细：\n本篇文章将主要介绍 zsh + oh-my-zsh 的安装、提示符主题配置，以及介绍 7 提升效率的 zsh 插件。\n为什么使用 zsh？ # 开始前，先问为什么，知其然，要知其所以然，是个好习惯。\n所以，为什么要用 zsh 呢？\n大家最熟悉的 shell 解释器，肯定是 bash。zsh（Z Sehll）相对于 bash（Bourne Again Shell）相对有哪些优势呢？\n改进的自动补全能力 # zsh 提供了更强大、更灵活的自动补全功能。它不但可以自动补全命令，设置选项、参数甚至文件名，都可自动补全。\n对于命令参数，zsh 甚至可以显示简短的帮助信息，这使得探索新命令变得更加容易。\n更好的脚本和插件支持 # zsh 有一个强大的社区，提供了大量的插件和主题，如 oh-my-zsh 这个流行的 zsh 框架，允许我们轻松添加、更新插件和主题。\n这些插件可以增强 shell 的功能，提供便捷的别名、函数以及其他有用的特性。\n高级的主题和提示符定制 # zsh 还允许用户对命令行提示符进行高度定制，包括颜色、内容和格式。用户可以非常容易地调整提示符来显示 git 分支、Python 虚拟环境等信息。\n我们会在后续介绍一款非常强大的 zsh 插件，名为 powerlevel10k，它支持完全的主题自定义特性，非常强大。\n更智能的命令行交互 # zsh 还支持 bash 不具备的一些智能特性，如拼写校正和近似完成。如果用户输入的命令有拼写错误，zsh 可以建议正确的命令。\n如我输入 lls，会提示我 \u0026ldquo;zsh: correct \u0026rsquo;lls\u0026rsquo; to \u0026rsquo;ls\u0026rsquo; [nyae]?\u0026rdquo;\n❯ lls zsh: correct \u0026#39;lls\u0026#39; to \u0026#39;ls\u0026#39; [nyae]? 输入 y 接受纠正建议。\n当然这个是要做个简单的配置，通过 setopt CORRECT_ALL 启用。\n其他 # 其他还有很多强大特性。如：\nzsh 的命令行历史是终端间共享的，通过自动补全，能一步增强了操作效率与体验。\nzsh 的文件匹配和通配符功能确实比 Bash 要强大得多，除了常规的通配符能力，还提供了一些扩展通配符、限定符等，如递归匹配 **/，ls **/*.go 会列出所有的 Go 文件。!{pattern}，匹配不符合模式的内容。其他更多自行探索。\nzsh 的可配置性更强，zsh 提供了比 bash 更多的选项和特性，我们都可通过配置文件调整。\n如果你想深入学习 zsh，推荐 awesome-zsh-plugins 这个仓库。或者推荐看一个关于 zsh 的深度系列文章：\nConfiguring Zsh Without Dependencies A Guide to Zsh Expansion with Examples A Guide to the Zsh Completion with Examples A Guide to the Zsh Line Editor with Examples 好吧，前导部分写的有点长。本文还是注重实践，比较的部分就先写这么多。\n安装 # 对于不同系统，zsh 的安装命令，如下所示：\nDebian\napt install zsh Centos\nyum install -y zsh Arch Linux\npacman -S zsh Fedora\ndnf install zsh 对于 macOS 系统的用户，MacOS 的默认 shell 从 2019 开始以前替换为 zsh，该步骤可省略。可阅读：What is Zsh? Should You Use it? 其中有介绍为什么 2019 macOS 将默认的 shell 从 bash 切换到 zsh。\n我看下来，主要原因就是版权问题啦。\n如果你是个老古董，还是用 MacOS 2019 之前的系统，可通过如下命令安装：\nbrew install zsh 安装完成后，将 zsh 设置为默认 shell，命令如下所示：\nchsh -s /bin/zsh 通过如下命令检查下是否成功。\necho $SHELL zsh oh-my-zsh # oh-my-zsh 是用于管理 zsh 配置的轻量级框架，具有开箱即用的特点，而且它提供了大量内置插件。让我们用它快速配置 zsh 吧！\noh-my-zsh 这个名字起的很骚气的，大概就是下面这样表情。\n想表达的可能是，当别人看你用 oh-my-zsh 配置的终端，大概率发出 \u0026ldquo;wow! 你的终端太赞了！\u0026rdquo;\nOK，那下面让我们尝试让它赞起来吧。\n安装 # 首先，oh-my-zsh 的安装很简单。\n安装命令，如下所示：\nsh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; 安装后，就已经有一些默认效果，如命令行提示符的主题变化。\n这是默认的 oh-my-zsh 主题 \u0026ldquo;robbyrussell\u0026rdquo;。\n主题 # oh-my-zsh 提供了许多内置主题，可查看 themes 获取一系列的主题。\n我们可直接通过 ~/.zshrc 配置更新主题配置，将内容修改如下：\nZSH_THEME=\u0026#34;agnoster\u0026#34;` # 默认为 robbyrussell 执行 source ~/.zshrc 生效配置，就能看到主题效果。\n另外，oh-my-zsh 还提供了 random 主题，它会在 oh-my-zsh 内置主题中随机选择一个主题展示。只需编辑 ~/.zshrc，将 ZSH_THEME 更新为 random 即可。\n配置如下所示：\nZSH_THEME=\u0026#34;random\u0026#34; 演示效果，如下所示：\n说实话，我觉得没人会这么用吧。这明显很鸡肋的功能啊。\n内置插件 # 重点来了，接下来我们一起来看看 zsh 的效率神器 - 插件能力吧。\n我先给大家推荐 7 款常用的插件，其中 5 个是 oh-my-zsh 的内置插件。考虑内容不宜太长，下期会再推荐 6 个插件。\noh-my-zsh 提供的所有内置插件，都可以在仓库 ohmyzsh/ohmyzsh/plugins 中找到，每个插件都有相应的介绍文档。\n本教程将要介绍的 5 个 oh-my-zsh 内置插件，如下所示：\ngit，Git 插件，其实就是提供一些常用的 git 命令别名。 web-search，命令行打开搜索引擎，已支持大部分搜索引擎； jsontools，用于格式化 json 数据； z，基于历史访问目录的快速跳转； vi-mode，使用 vi 模式编辑命令行； 启用所有插件，打开 zshrc 配置，把这些内置插件都打开，如下所示：\nplugins=(git web-search jsontools z vi-mode) 插件 1 - git # Git 插件提供了 git 命令的大量别名，查看git 插件文档。\n如下一些常用命令的别名：\ngit clone -\u0026gt; gcl git status -\u0026gt; gst git commit -\u0026gt; gc git add -\u0026gt; ga git add --all -\u0026gt; gaa git diff -\u0026gt; gd git push -\u0026gt; gp git pull -\u0026gt; gl 更多命令的映射关键关系，可自行查看它的文档。\n这个插件不错，但有个缺点，这么多可用别名，我又记不住，岂不是成了摆设。如果想用好，我每次都用去查文档吗？不查文档行不行呢？\n当然也是可以的，oh-my-zsh 中启用的一些其他插件可能也会有别名。\n其实，有一个插件可帮忙我们解决这个问题，叫做 you-should-use，这是下期要介绍的一个插件。简单说下，它的作用是，当我们输入一个命令时，如果这个命令存在别名，它会提示我们要使用别名。\n插件 2 - web-search # web-search 提供了在终端直接搜索信息的能力。\n当然，其实也不是完全在终端完成，它会自动跳转浏览器，转到指定的搜索引擎执行搜索请求。\n效果大概就是下面这样：\n常见的搜索引擎基本都是支持的，诸如 google, bing, baidu, 甚至是 github 等。\n不过，我也得承认，其实这个插件一般我本人很少用，因为我已经安装了另外一个工具 alfred（替代 mac 默认的 spotlight），我都是通过它直接启动搜索。\n插件 3 - jsontools # 接下的这个插件，名为 jsontools ，即用于 json 的 tool。其实它只提供了一些操作 json 的基本命令，如下:\npp_json 实现 json 字符串格式化； is_json 判断是否是 json； 我们直接看下演示效果吧，如下所示：\n还是得说明，如果你没有更好的方案，安装了 oh-my-zsh，这是个不错的选择，因为你可能以前都没用过这类工具。\n不过其实这个插件呢？我也很少用。\n我习惯使用一款叫做 jq 的命令，如果你了解它，就知道它多强大。后面说到高效命令的时候，会介绍到它的。\n插件 4 - z # z 插件 可用于快速的目录跳转，我觉得大部分人在使用 Linux 都被 cd 跳转目录跳转烦恼过。\nz 就是这个烦恼的救星。\n想查看更多信息可找 z 原仓库 - zsh-z 查看。oh-my-zsh 下的 z 文档说明中提到，它是从这个 zsh-z 的插件中拷贝而来的。\n我们来介绍它的用法，简单来说，它是基于历史访问过的目录快速跳转。我们无需输入全路径，即可完成目录切换。\n下面是一些实际案例。\n首先，我直接输入 z，紧跟 tab 键，会看到如下的效果。它会直接将访问过的目录都列出来。\n这些由 tab 产生的自动补全目录都是历史访问过的目录。因为，在没有输入任何内容的情况下，我们输入 tab 的，它列出最近访问过的目录。\n如果我们输入形如 z substring，即提供子字符串，它们将所有匹配 substring 的目录都列举出来。\n效果如下：\n例如，我们输入 z blog，紧跟 tab 键，会直接列出访问过包含 blog 的目录。\n如果输入内容只有一个关联的目录名，它会如图上一样直接补全。\n演示效果：\n我们输入 z tmux，因为匹配 tmux 的目录只有一个，将会被直接选中。\n当然，其实这里匹配的目录名只有一个，直接输入 Enter 就可以进入目录，无需 tab 选择多次一举了。\n演示效果：\n我们输入 z tmux，直接 Enter 确认，即可进入到目录。\nz 非常强大是吧？\n其实，有一款更强大的命令，名为 zoxide，也提供了类似的能力，它的灵感是来源于 z。我一般用的是它，后面我会介绍。\n当然，这不妨碍你继续使用 oh-my-zsh 内置的工具 z，毕竟它很容易配置。\n插件 5 - vi-mode # vi-mode 插件 支持在命令行开启 vi 模式，利用 vi 键进行命令行编辑。这个插件，视个人情况，是否使用吧。如果你是一个 vi 忠实用户，可考虑开启。否则，还是简单最好，否则容易影响心情。\n这个插件就不多介绍了，更多查看 它的文档。另外，如果确实对 vim 感兴趣，也可以考虑另外一个 vi 插件，名为 zsh-vi-mode，它的能力更强大，也解决这个默认 vi 插件的一些不好用的 bug，不过它的配置有点复杂。\n三方插件 # 我们再来了解 2 个非 oh-my-zsh 内置插件，即 zsh-syntax-highlighting 和 zsh-autosuggestions。这两个插件由 zsh 社区开发。\n开始介绍前，先将这两个插件全部安装配置完成。\n下载 # 下载命令如下所示：\ngit clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/zsh-autosuggestions git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting 配置 # 打开 .zshrc 完成配置：\nplugins=(git web-search jsontools z vi-mode zsh-syntax-highlighting zsh-autosuggestions) 记得执行 source ~/.zshrc 生效配置。\n插件 6 - zsh-syntax-highlighting # zsh-syntax-highlighting 是 zsh 的语法高亮插件，如果输入的命令不存在，或者输入 shell 语法不正确，将会自动以红色表示。它的优点就是，当我们在终端输入，实时输入实时反馈。\n首先，我们尝试下错误命令，提示效果，如下所示：\n再来看看，正确命令提示效果，如下所示：\n对，就是这么简单。通过这个插件提供的实时反馈，可以防止我们在命令执行后，才知道输入错了。\n插件 7 - zsh-autosuggestions # zsh-autosuggestions 可以说是我最喜欢的插件了。\n它的作用是什么呢？\n它可用于提示补全建议，当输入字符，默认情况下，它基于我们的历史命令自动提供输入建议。还记得前面提到的，zsh 的历史命令是在不同的会话间共享。现在，再结合 zsh-autosuggestions 插件，简直不要太爽。哈哈。\n我们先看下效果，如下所示：\n默认情况下，输入右方向键 → 可将建议直接输入终端。\n但这个其实体验很差，对于一个双手不想离开键盘中心区域的人而言，通过右键接受提示建议，这简直不能忍啊。是否能改变这个默认快捷键呢？\n我的目标是希望通过输入 Ctrl + / 接受建议，配置实现，如下所示：\n# \u0026lt;Ctrl+/\u0026gt; 接受 auto-suggestion 的补全建议 bindkey \u0026#39;^_\u0026#39; autosuggest-accept 对！不要怀疑，CTRL+/ 的的字符表示就是 \u0026lsquo;^_\u0026rsquo;，我们可以通过执行 cat 命令查看，输入 CTRL+/，会到看如下输出。\n如果你不知道想要设置的快捷键的字符表示，可以通过这种方式找到。\n另外，如果希望 zsh-autosuggestion 不仅支持 history，也支持自动补全的建议提示，即原来那些要输入 tab 才能出现的内容，如子命令、命令选项、目录文件等提示，也能在提示建议的范围中。我们只需增加 completeion 这个配置项。\n如下所示：\nexport ZSH_AUTOSUGGEST_STRATEGY=(history completion) 现在，如果输入时，还没有历史命令可作为建议，会提供类似于目录、参数选项等建议。\n类似于如下的效果：\n这是一篇含 GIF 量很高的文章啊。\n总结 # 本文想要介绍的内容已完成。我们从 zsh 与 bash 对比，了解到 zsh 的强大。接着开始实操，从安装 zsh，oh-my-zsh、主题配置，到介绍 5 个内置插件，2 个三方插件。\n最后，希望本文能对你的终端操作效率提到一点点帮助，我就心满意足了。\n我的博文：我的终端环境：zsh、oh-my-zsh，提示主题和 7 个效率插件\n","date":"2023-10-16","externalUrl":null,"permalink":"/posts/2023-10-16-zsh-themes-and-plugins/","section":"文章","summary":"前文中，对 iTerm2 已经有了一个大概认识。但一个高效的终端环境，离不开一个优秀 shell 解释器。\n视频版本，没有文章详细：\n本篇文章将主要介绍 zsh + oh-my-zsh 的安装、提示符主题配置，以及介绍 7 提升效率的 zsh 插件。\n","title":"终端环境：zsh 、oh-my-zsh、提示主题与 7 效率插件","type":"posts"},{"content":"","date":"2023-10-10","externalUrl":null,"permalink":"/tags/golang/","section":"Tags","summary":"","title":"Golang","type":"tags"},{"content":"Python 中，如果想要表示多行字符串，只要通过三单/双引号(\u0026quot;\u0026quot;\u0026quot;）包裹字符串即可。\n类似代码，如下所示。\na = \u0026#34;\u0026#34;\u0026#34;line1 line2 line3\u0026#34;\u0026#34;\u0026#34; print(a) 执行代码，查看输出效果，如下所示：\nline1 line2 line3 Golang 中如何实现呢？不复杂，简单展示两种方式：\n方式 1. 通过 ` 符号 # 具体代码如下：\nstr := `hello world` 这种方式的性能最优。\n方式 2. 通过 + 号进行拼接。 # Golang 支持通过 + 拼接字符串，如下所示：\ns := \u0026#34;hello\u0026#34; + \u0026#34;world\u0026#34; fmt.Print(s) 输出如下所示：\nhelloworld 从输出结果已经看出来，没有换行效果。对于使用 + 拼接，需要使用 \\n 转义符，进行换行。\n代码如下所示：\ns := \u0026#34;hello\\n\u0026#34; + \u0026#34;world\u0026#34; fmt.Println(s) 输出如下所示：\nhello world 是我们期望的结果。\n搞定！\n","date":"2023-10-10","externalUrl":null,"permalink":"/posts/2023-10-10-multi-lines-string-in-golang/","section":"文章","summary":"Python 中，如果想要表示多行字符串，只要通过三单/双引号(\"\"\"）包裹字符串即可。\n类似代码，如下所示。\na = \"\"\"line1 line2 line3\"\"\" print(a) 执行代码，查看输出效果，如下所示：\n","title":"Golang 中如何实现多行字符串","type":"posts"},{"content":"","date":"2023-10-09","externalUrl":null,"permalink":"/tags/web/","section":"Tags","summary":"","title":"Web","type":"tags"},{"content":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。希望通过翻译这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n本文是 \u0026ldquo;构建属于自己的 Web 框架\u0026rdquo; 系列文章中的第四篇，将介绍如何在 Go 中使用三方路由。\n第 1 部分：简介，Build Your Own Web Framework In Go 第 2 部分：Go 中间件：最佳实践和示例，Part 2: Middlewares in Go: Best practices and examples 第 3 部分：中间件数据共享，Part 3: Share Values Between Middlewares 第 4 部分：第三方路由，Part 4: Guide to 3rd Party Routers in Golang 第 5 部分：使用 MongoDB 实现 JSON-API，How to implement JSON-API standard in MongoDB and Go 基于 Go 标准库 net/http，已经足够写出一个 Web 应用。但不足的是，它提供的路由能力 http.Handle(pattern, handler) 还是过于单一，只能实现一些静态路由。\n这就是为什么我们需要一个优秀的三方路由。\n然如此多的第三方路由，都有各自的特点，究竟该如何选择？\n当接触一门新的编程语言，如果有 10 个不同库实现相同能力，将很难了解什么是最佳实践。我们希望有一种速度快、内存高效且易于使用的 router。\n如下是我认为 Go 中最常用的 router，将从执行速度、内存消耗等维度对比。\ngorilla/mux # gorilla/mux 是一款成熟的 router，同时也是 Go 中最流行的三方路由。它有着丰富的功能，缺点是速度慢且内存消耗验证。\n且，gorilla/mux 支持正则 URL 参数约束，如下所示：\nr := mux.NewRouter() r.HandleFunc(\u0026#34;/teas/{category}/\u0026#34;, TeasCategoryHandler) r.HandleFunc(\u0026#34;/teas/{category}/{id:[0-9]+}\u0026#34;, TeaHandler) HTTP 方法配置路由，如下：\nr.Methods(\u0026#34;GET\u0026#34;, \u0026#34;HEAD\u0026#34;).HandleFunc(\u0026#34;/teas/{category}/\u0026#34;, TeasCategoryHandler) 和其他路由的不同，gorilla/mux 有丰富的内置匹配规则，支持如 host（如子域名）、前缀、协议（http、https 等）、HTTP 头、查询参数。如果这些还不能满足你，通过自定义方式，如下方式：\n// Proto string // \u0026#34;HTTP/1.0\u0026#34; // ProtoMajor int // 1 // ProtoMinor int // 0 r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { return r.ProtoMajor == 0 }) 在 Handler 函数中，通过 mux.Vars(request) 可获取 URL 参数，它和上文介绍的 gorilla/context 类似。\n代码如下：\nfunc myHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) category := vars[\u0026#34;category\u0026#34;] } 这个方案的优势是，它与 http.Handler 接口兼容。这点其实非常重要，因为我们的应用越多，共享 handler 和 middleware 的可能越大，就更加需要遵循一定的规则。\n优势：功能强大，轻松创建复杂的路由规则，且与 http.Handler 兼容。 劣势：速度慢且内存消耗严重，如果看中速度的话，它不适合你。\nhttprouter # httprouter, 号称 \u0026ldquo;最快的 router\u0026rdquo;。httprouter 的作者对不同的 router 做了基准测试，具体查看 go-http-routing-benchmark。\nhttprouter 比 gorilla/mux 简单，但它不支持约束和正则，对于 REST API 而言，这个缺点的影响不大，但如果希望创建复杂的路由，这个简化设计就会大大限制它的适用范围。\n还有，它与 http.Handler 不兼容，它定义了一个新的 interface，拥有三个参数，其中第三个参数用于访问 URL 参数。\n示例代码：\nfunc Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, \u0026#34;Welcome!\\n\u0026#34;) } func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, \u0026#34;hello, %s!\\n\u0026#34;, ps.ByName(\u0026#34;name\u0026#34;)) } func main() { router := httprouter.New() router.GET(\u0026#34;/\u0026#34;, Index) router.GET(\u0026#34;/hello/:name\u0026#34;, Hello) } 但这个问题也容易解决，将 URL 参数注入 context 中，实现在标准 interface http.Handler 和 httprouter 接口间的转换。这种方式会损失一些性能，但依然是一个 faster router。\n如何实现？后续具体实现时介绍。\n优点：快。\n缺点：与 http.Handler 不兼容。\nPat # Pat 也是一个流行且简单的 router。它与 http.Handler 完全兼容。但它不是用 context 存储 URL 参数，而是将参数保存在 request 中，通过 r.URL.Query() 获取。\n示例代码：\nfunc Hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;hello, %s!\\n\u0026#34;, r.URL.Query().Get(\u0026#34;:name\u0026#34;)) } func main() { m := pat.New() m.Get(\u0026#34;/hello/:name\u0026#34;, http.HandlerFunc(Hello)) } 缺点是 r.URL.Query() 每次都是从原始 querystring 中解析参数，这是对性能不友好的行为，如果包含经过多个中间件，这对性能的影响将更大。速度方面，pat 相较于 httprouter 要慢十倍。\n优势: 与 http.Handle 兼容.\n劣势: 有点慢.\n如何选择？ # 如果是传统 Web 应用，服务端进行页面渲染，因为需要复杂的路由，gorilla/mux 是最好的选择。如果是 REST API，httprouter 更加适用，因而，我们将基于 httprouter 完善我们的程序。\n集成 httprouter # 由于 httprouter 与 http.Handler 不兼容，要进行一些调整。实现方案，将中间件栈（http.Handler）包裹，从而实现 httprouter.Handler 接口。\n代码如下：\nfunc wrapHandler(h http.Handler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { context.Set(r, \u0026#34;params\u0026#34;, ps) h.ServeHTTP(w, r) } } func main() { db := sql.Open(\u0026#34;postgres\u0026#34;, \u0026#34;...\u0026#34;) appC := appContext{db} commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler) router := httprouter.New() router.GET(\u0026#34;/admin\u0026#34;, wrapHandler(commonHandlers.Append(appC.authHandler).ThenFunc(appC.adminHandler))) router.GET(\u0026#34;/about\u0026#34;, wrapHandler(commonHandlers.ThenFunc(aboutHandler))) router.GET(\u0026#34;/\u0026#34;, wrapHandler(commonHandlers.ThenFunc(indexHandler))) http.ListenAndServe(\u0026#34;:8080\u0026#34;, router) } 通过 wrapHandler 实现将中间件 http.Handler 和 httprouter.Hande 间的转化，从而实现拥有 httprouter 的良好性能的同时，也能与http.Handler的兼容。\n接下来，演示如何在 handler 使用 URL 参数。\n创建路由：\nrouter.GET(\u0026#34;/teas/:id\u0026#34;, wrapHandler(commonHandlers.ThenFunc(appC.teaHandler))) 如下代码，创建 teaHandler，其中将通过 id 从数据库中查询数据。\nfunc (c *appContext) teaHandler(w http.ResponseWriter, r *http.Request) { params := context.Get(r, \u0026#34;params\u0026#34;).(httprouter.Params) tea := getTea(c.db, params.ByName(\u0026#34;id\u0026#34;)) json.NewEncoder(w).Encode(tea) } 总结 # Go 中的不同 router 的性能差异很大，功能也有差异。最快的路由器并不一定适合你的项目。httprouter 非常适合于 REST API 这样的简单路由，gorilla/mux 更适合具传统的 web 应用。\n对于不兼容与 http.Handler 的路由实现，可通过类似 wrapHandler 实现兼容。\n最后，不同 router 方案存储 URL 参数的方式不同，常见的两种方式： r.URL.Query() 和 context。在实际使用时，要注意规范一致。\n我的博文：从头构建 Go Web 框架（四）：第三方路由集成 ，原文地址: Part 4: Guide to 3rd Party Routers in Go\n","date":"2023-10-09","externalUrl":null,"permalink":"/posts/2023-09-30-build-your-own-webframework-in-golang-part-4/","section":"文章","summary":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。希望通过翻译这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n","title":"从头构建 Go Web 框架（四）：第三方路由集成","type":"posts"},{"content":"Jian Xue（Polo Xue, +8615312210823, poloxue123@gmail.com)\nBlog: https://www.poloxue.com, GitHub: https://github.com/poloxue\nSelf Description # I\u0026rsquo;m a senior backend engineer, and have ten years of experience in server development, I\u0026rsquo;m familar with Golang/Python/PHP/Javascript/C++/AWS/Kubernetes/GitLab, etc.\nI have three years\u0026rsquo; experience in leading projects and teams, understanding how to use Agile and DevOps, and having experience in multiple projects from scratch.\nBusiness areas mainly focus on financial industries, such as exchanges, cryptocurrencies, fund sales, financial education, quantitative trading, Web3, etc.\nCurrently, I\u0026rsquo;m in China and prefer an English remote full-time job. I\u0026rsquo;m proficient in Mandarin and able to communicate in English.\nWork Experiences # Bybit, 2019.12 - 2022.12, 3 Years # Senior Backend Expert\nThree times as the technical owner of the WSOT, a trading competition project.\nResponsible for or participated in almost all growth projects (Referrer, Affiliate, TaskCenter, and Campaign, etc).\nUpdate all growth systems from monolithic to microservice architecture.\nIn charge of a back-end team and increase its size from 15 to 50 people.\nParticipate in creating the ByFi team and designing the architecture of the first product - Cloud Mining.\nPerformance in three years, be rated S one time and A 2 times, witnessing Bybit’s 3-year golden period.\nNoah, 2016.05 - 2019.10, 3.5. Years # Senior backend developer.\nMainly responsible for search feature of a financial consulting APP using canal and ElasticSearch.\nResponsible for an algorithm trading system that involved data, backtesting, analysis, and live trading to assist researchers in developing strategies.\nImprove the performance of the fund PNL update system for a fund APP by combining crawlers and third-party data. It is about 3 hours faster than before.\nDevelop message services, including SMS, email, and app push. Through multi-channel aggregation, the SMS reach rate is improved from 95% to over 98%.\nHuo, 2015.08-2016.05, 9 months # PHP developer.\nResponsible for a multi-channel aggregation project for movie ticketing with more than ten ticketing channels.\nHuilian, 2015.01-2015.08, 8 months # PHP Engineer, responsible for developing the travel ticket service and integrating gates into ticketing systems.\nDuring this period, I learned lots of things about web development.\nCynovo, 2013.03 - 2014.09, 1.5 Years # Embedded Engineer.\nCynovo is the first company I joined after graduation, I was independently responsible for the research and development of multiple electronic scale projects. I\u0026rsquo;m responsible for all work involving the embedded driver, app, and web server.\nDuring this period, I started to learn web development.\nEducational experience # 2010-2013, Jiangsu Maritime, Electronic Information Engineering.\nAt last, since quitting Bybit, I didn\u0026rsquo;t start to find a job immediately. I\u0026rsquo;ve been studying English and an interesting thing - quantitative trading for eight months. I want an opportunity to work in an English environment.\n","date":"2023-10-04","externalUrl":null,"permalink":"/resumes/2023-10-04-resume-en/","section":"Resumes","summary":"Jian Xue（Polo Xue, +8615312210823, poloxue123@gmail.com)\nBlog: https://www.poloxue.com, GitHub: https://github.com/poloxue\nSelf Description # I’m a senior backend engineer, and have ten years of experience in server development, I’m familar with Golang/Python/PHP/Javascript/C++/AWS/Kubernetes/GitLab, etc.\nI have three years’ experience in leading projects and teams, understanding how to use Agile and DevOps, and having experience in multiple projects from scratch.\nBusiness areas mainly focus on financial industries, such as exchanges, cryptocurrencies, fund sales, financial education, quantitative trading, Web3, etc.\n","title":"English Resume","type":"resumes"},{"content":"","date":"2023-10-04","externalUrl":null,"permalink":"/resumes/","section":"Resumes","summary":"","title":"Resumes","type":"resumes"},{"content":"薛建（+8615312210823, poloxue123@gmail.com)\nBlog: https://www.poloxue.com, GitHub: https://github.com/poloxue\n自我介绍 # 目前在国内，优选远程全职开发机会，如果英语环境会更好。\n10 年服务端经验，技术栈 Golang/Python/PHP/Javascript/C++/GRPC/AWS/Kubernetes/GitLab 等；\n3 年带项目和团队经验，多次主导或参与从 0 到 1 项目的研发，熟悉敏捷和 devops；\n业务领域主要围绕金融方向，诸如交易所、数字货币、基金代销、理财教育、量化交易、Web3 等方向；\n有技术热情，爱分享，爱开源，工作之余不定期输出技术文章，查看 我的博客；\n语言方面，熟悉的中文普通话，不熟练但可日常交流的英语，已坚持学习英语一年；\n工作经历 # Bybit, 2019.12 - 2022.12，3 Years # 曾 3 次带队研发公司级项目 WSOT（全球交易大赛），将大赛能力产品化，完成配置化改造和性能提升。 主导或参与 bybit 几乎全部增长方向项目，如大赛、邀请、代理商、卡券任务、赠金返佣、运营活动等。\n推进技术框架与规范升级，系统架构从单体升级到微服务架构，部署方式从 ec2 升级为 k8s。 推进多部门横向项目，参与交易系统升级，快速上币、测试环境与发布流程治理等。\n组建部门后端团队，培养骨干若干，团队规模从 15 人到 50 人。参与组建 bybit 理财团队，完成了第一个理财产品的架构设计和人员组建。\n表现方面，5 次评分 1 次 S，2 次 A，见证 bybit 的 3 年发展黄金期，交易量从 20 亿到 400 亿，人员从 100 到 2500 人。\n诺亚财富，2016.05 - 2019.10, 3.5. Years # 负责开发理财咨询类 APP 下的话题与搜索类模块，实现基于 canal 的数据同步与 elasticsearch 的搜索算法。此期间，为提升 Go 理解深度，写了超过 30 篇 Golang 原创文章，查看 我的知乎 或个人博客。\n负责量化系统的开发，基于开源框架和公司内部系统，搭建了一套集数据、回测、分析与实盘的量化系统，辅助研究员的策略研发，加快策略与业务的对接。此期间，参与 tushare 开源项目，协助 tushare 作者 jimmy 将 tushare 社区版 升级为 tushare 专业版。\n负责升级基金 APP 净值更新系统，将原依赖三方数据的方案升级为爬虫与三方数据相结合的方案，净值更新时间从三方数据源的零时跟新，提升到与基金公司同步，提前至少约 3 小时。\n负责统一触达服务，包括 sms、email, app push，通过多通道聚合，将 sms 触达率从 95% 提升到 98% 以上，同时，基于建立 ELK 报警体系缩短报警响应时间。\n火传媒, 2015.08-2016.05, 9 months # PHP 高级开发，主导开发了一套电影票务多渠道聚合接口，对接超过 10 家票务系统，提高上层电影票务应用的研发效率。\n汇联票务, 2015.01-2015.08, 8 months # PHP 工程师，负责开发旅游票务管理系统，并独立负责系统与闸机的对接。此期间，完成我的 web 知识体系的升级。\nCynovo, 2013.03 - 2014.09, 1.5 Years # 嵌入式开发，毕业后入职的第一家公司，独立负责多个电子秤相关项目研发，从驱动、APP 、服务端，全部独立完成。此期间，开始接触 web 开发。\n教育经历 # 2010-2013 年，江苏海事，电子信息工程。\n","date":"2023-10-04","externalUrl":null,"permalink":"/resumes/2023-10-04-resume-zh/","section":"Resumes","summary":"薛建（+8615312210823, poloxue123@gmail.com)\nBlog: https://www.poloxue.com, GitHub: https://github.com/poloxue\n自我介绍 # 目前在国内，优选远程全职开发机会，如果英语环境会更好。\n","title":"Chinese Resume","type":"resumes"},{"content":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。系统通过这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n本文是 \u0026ldquo;构建属于自己的 Web 框架\u0026rdquo; 系列文章中的第二篇，将介绍中间件的最佳实践。\n第 1 部分：简介，Build Your Own Web Framework In Go 第 2 部分：Go 中间件：最佳实践和示例，Part 2: Middlewares in Go: Best practices and examples 第 3 部分：中间件数据共享，Part 3: Share Values Between Middlewares 第 4 部分：第三方路由，Part 4: Guide to 3rd Party Routers in Golang 第 5 部分：使用 MongoDB 实现 JSON-API，How to implement JSON-API standard in MongoDB and Go 附加福利：上传文件到 s3，Bonus: File Upload REST API with Go and Amazon S3 我们在 上文 中介绍了 middleware 的实现，通过创建 func (http.Handler) http.Handler 类型函数，实现了路由或应用间共享代码。案例: 日志（logging） 和 异常恢复（panic recovery）。但是，这两个案例的功能还是相对单一，多数中间件要比它们复杂，需要在中间件间共享数据。\n本文将以用户身份验证为例，介绍如何在 middleware 间共享数据。\n前言 # 对于面向用户的 web 服务而言，用户认证是几乎每个 request handler 都需要的能力，而 middleware 正是为此而生。\n如果没有 middleware，则需要在每个 handler 中查询数据库得到用户信息，如用户 ID、名称等，认证用户是否合法等。而利用 middleware，则能统一处理。但问题是，下游 handler 还能得到登录用户信息。\n如果能解决了 middleware 与 handler 间的数据共享问题，就能实现我们的目标。\n常见思路 # Ruby 中的 Rack 可通过 hash map 类型 env 变量存储一个请求周期内的信息。我们也可以用它保存信息。\nJavascript 的 Connect.js 中间件中有 2 个参数，分别是 request 和 response。request 是个 object，在 javascript 也可以是 map，我们可以用它来存储请求范围内的中间信息。\n静态语言也没有什么不同，如 Java 的 Netty 也是通过 map 实现。handler 通过访问 context 中的一个 map 属性实现数据共享。\nGo 中的一些框架和包是如何做的呢？\nGorilla # 和上面的类似，Gorilla 的 gorilla/context 中定义了一个变量 map[interface{}]interface{}，可用于存储数据，同时通过 mutex 保证线程安全。\n定义代码，如下所示：\nvar ( mutex sync.RWMutex data = make(map[*http.Request]map[interface{}]interface{}) ) 如何存储与获取：\nfunc myHandler(w http.ResponseWriter, r *http.Request) { context.Set(r, \u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;) } func myOtherHandler(w http.ResponseWriter, r *http.Request) { val := context.Get(r, \u0026#34;foo\u0026#34;).(string) } 因为 context 中 data 中不会被自动清理。如果每个请求都向其新增数据，它将会无限增加。因而，在请求结束后要对 map 进行清理。\n优点：\nhandler 和 middleware 保持不变，调用函数存储和读取即可。 缺点：\n基准测试显示它是性能最差的方案，但实际应用中，性能通常不是核心点； 使用 mutex，而非 channel， 但 Go 文档并不反对 mutex。 对于 interface{} 类型，需要经过类型断言； Goji # Goji 使用一个含有 map[string]interface{} 名为 Env 的结构体，无互斥锁。\nfunc myMiddleware(c *web.C, h http.Handler) http.Handler { fn := func (w http.ResponseWriter, r *http.Request) { c.Env[\u0026#34;name\u0026#34;] = \u0026#34;world\u0026#34; h.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func hello(c web.C, w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Hello, %s!\u0026#34;, c.Env[\u0026#34;name\u0026#34;].(string)) } 优点：\n无互斥锁； 缺点：\n无法与我们现在的中间件兼容； 需要类型断言； gocraft/web # gocraft/web 中可以用任何类型作为 context 共享数据。没有 map，无类型断言。对于性能来说非常友好，但它是最不兼容的方案。每个应用的 context 都不同的，这将导致你的中间件无法被不同系统复用。\n优点：\n无互斥锁； 无类型断言； 缺点：\n无法与我们的中间件兼容； 为此框架而写的代码难以复用； go.net/context # Google 内部 context 包。阅读 context。一个非常好的解决方案，他们想统一 context 的实现。问题是它是否能与我们的系统很好地配合呢？\n汇总对比 # 不同方案对比，如下所示：\npackage mutex map struct go.net/context n y* n goji n y n gin n y n martini n y n gorilla y y n tigertonic y n y gocraft/web n n y go.net/context 未使用 map，但方法类似。\ncontext 的实现思路都是类似的，但也各有优劣。\n为提高性能，一些方案在尽量避免 type assertion、map 或 mutex，但其实性能差异不到 10%。虽然，struct 是最快的，但如果一个请求要 10 ms 的处理时间，这种影响将不到 1%。\n毫无疑问，最灵活的实现方案是 map。使用 map 的 context 是 gorilla/context 和 Goji。\ngorilla/context 是某位 Go 创建者的方案，也是最容易实现的。\n本文将基于 gorilla/context 实现。\n如果想将自己的成果分享他人，则要尽量采用标准接口。如 nosurf 的 context，与 gorilla/context 是相同概念。在几乎所有框架的项目中使用，而不会出现问题。\n这也是为什么 gorilla/context 系统更优秀，它更易重用中间件。\ncontext 集成 # 使用 gorilla/context，我们几乎无需对代码进行任何修改。\n在介绍 gorilla/context 时，我说过 context 中的 map 不会自动清理，因此，对于每个 request，map 中都会有新的内容且无限增加。该 package 提供了一个 ClearHandler 解决这问题。\nfunc main() { commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler) http.Handle(\u0026#34;/about\u0026#34;, commonHandlers.ThenFunc(aboutHandler)) http.Handle(\u0026#34;/\u0026#34;, commonHandlers.ThenFunc(indexHandler)) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 让我们创建一个身份认证 middleware，authhandler，用于用户验证并存储用户信息。\n从 header 中获取 token，基于 token 从 database 查找用户。如果认证失败，将返回错误，而不是继续执行下一个 middleware。反之，它将把所获得的用户信息存储在 context，并调用下一个 middleware。\nfunc authHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { authToken := r.Header().Get(\u0026#34;Authorization\u0026#34;) user, err := getUser(authToken) if err != nil { http.Error(w, http.StatusText(401), 401) return } context.Set(r, \u0026#34;user\u0026#34;, user) next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func adminHandler(w http.ResponseWriter, r *http.Requests) { user := context.Get(r, \u0026#34;user\u0026#34;) json.NewEncoder(w).Encode(user) } func main() { commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler) http.Handle(\u0026#34;/admin\u0026#34;, commonHandlers.Append(authHandler).ThenFunc(adminHandler)) http.Handle(\u0026#34;/about\u0026#34;, commonHandlers.ThenFunc(aboutHandler)) http.Handle(\u0026#34;/\u0026#34;, commonHandlers.ThenFunc(indexHandler)) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 为了简化演示，假设 user 类型是 map[string]interface{}。\n在 AdminHandler 中，我们获取 context 中的用户信息，并写入 Response 中。\n应用级别变量 # 这种方式的问题是， getUser 要访问数据库，而 context 是 request 级别，在每个 request 的 context 中存储数据库连接不是一种好的实现方式。最好的方式是，所有请求共享一个 DB 连接。\n如下所示，使用 global/package 变量：\nvar dbConn *sql.DB func main() { dbConn := sql.Open(\u0026#34;postgres\u0026#34;, \u0026#34;...\u0026#34;) } 应用程序中的任何地方都可访问 dbConn，且 *sql.DB 是一个并发安全的连接池。以往经验，global 变量不利于维护，存在容易修改，无法追踪的问题。即使有完全的测试，也存在难以重构的问题。\n另一个方案是，创建一个 struct 变量，管理类似 dbConn 的变量，同时将 handler 和 middle 作为它的方法。为解决上述问题，它将包含 db 字段，方法有 authHandler 和 adminHandler。\n稍作变动，即将 db 用于 getUser 函数，如下所示：\ntype appContext struct { db *sql.DB } func (c *appContext) authHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { authToken := r.Header.Get(\u0026#34;Authorization\u0026#34;) user, err := getUser(c.db, authToken) if err != nil { http.Error(w, http.StatusText(401), 401) return } context.Set(r, \u0026#34;user\u0026#34;, user) next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func (c *appContext) adminHandler(w http.ResponseWriter, r *http.Request) { user := context.Get(r, \u0026#34;user\u0026#34;) // Maybe other operations on the database json.NewEncoder(w).Encode(user) } func main() { db := sql.Open(\u0026#34;postgres\u0026#34;, \u0026#34;...\u0026#34;) appC := appContext{db} commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler) http.Handle(\u0026#34;/admin\u0026#34;, commonHandlers.Append(appC.authHandler).ThenFunc(appC.adminHandler)) http.Handle(\u0026#34;/about\u0026#34;, commonHandlers.ThenFunc(aboutHandler)) http.Handle(\u0026#34;/\u0026#34;, commonHandlers.ThenFunc(indexHandler)) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 它与中间件系统很好地契合，代码变动很小。类似 gocraft/web 的视线，使用一个 struct 管理应用级别变量，但仅适用于应用程序范围。\n可以将 getUser 挂到 appContext，使代码更加简洁。或将 *sql.DB 包含在其他自定义结构中，将 getUser 添加为这个自定义结构的方法，使用 c.db.getUser(token) 即能完成调用。\n最后 # 本文通过 middleware 的数据共享，实现了将用户数据传递到主 handler。实现了以最小的修改将 middleare 和支持 context 的中间件相结合。\n下一章主题：router 路由。\n","date":"2023-09-29","externalUrl":null,"permalink":"/posts/2023-09-30-build-your-own-webframework-in-golang-part-3/","section":"文章","summary":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。系统通过这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n","title":"从头构建 Go Web 框架（三）：中间件的数据共享","type":"posts"},{"content":"","date":"2023-09-28","externalUrl":null,"permalink":"/tags/iterm2/","section":"Tags","summary":"","title":"Iterm2","type":"tags"},{"content":"视频版本，没有文章详细：\n我想大部分程序员在平时的工作中都是离不开终端，特别是如果你的系统是 MacOS 或 Linux 的话，终端的地位更是遥遥领先。\n本系列的目标是介绍如何基于 iTerm2、zsh、oh-my-zsh（包括高效插件）、高效 shell 命令，甚至将计划基于 Tmux 和 Neovim 搭建我的日常终端环境。\n本文是搭建我的终端环境系列中的第一篇，首先将介绍第一个必不可可少的工具终端 - iTerm2，Mac 上的终端神器。\n前言介绍 # 我将主要介绍如何安装与配置 iTerm2，安装成功后，会带着一起体验的一些能力。\n首先，iTerm2 是一款终端软件，它是 macOS 下默认终端 Terminal 的替代品。每次拿到新电脑，或者因某种原因重装系统，我首先要做的就是下载 iTerm2 来替换默认的终端 terminal。\niTerm2 vs 默认 Terminal # 为什么要用 iTerm2 替换默认的系统终端呢？这总要一些原因吧。\n首先，iTerm2 相较于 Terminal 的优势就是，它更加美观，相对于默认终端，iTerm2 支持真彩，而且，你可以在终端显示图片，甚至是 gif 动图。\n其他功能如分屏能力、颜色面板主题配置、搜索等肯定是基本能力，但毫无疑问，比默认终端体验更友好，更优秀。还有，快捷键的定制性更强。展示静态图片和 GIF 也不在话下。\niTerm2 还支持如 python 编程控制，可实现自动换背景，布局管理等各种自动化能力。\n还有，与 iTerm2 与 zsh 相结合体验更佳，zsh 部分会在后面慢慢介绍。\n废话不多说，接下来，让我们进入安装使用流程。\n下载安装 # 首先是安装，可通过 iTerm2 官网 下载或者 MacOS 中 brew 安装，我将以 brew 安装为例。\n如果还未安装 brew，安装命令：\n/bin/bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\u0026#34; 安装 iterm2，命令如下：\nbrew install --cask iterm2 等待执行完成，即安装完毕。\n颜色面板主题 # iTerm2 支持自定义颜色面板 color preset 设置。\n首先，以 material design colors 为例，介绍如何安装设置。\n安装命令，如下所示：\ncurl -Ls https://raw.githubusercontent.com/MartinSeeler/iterm2-material-design/master/material-design-colors.itermcolors \u0026gt; /tmp/material-design-colors.itermcolors \u0026amp;\u0026amp; open /tmp/material-design-colors.itermcolors 如果成功执行，将会在 iTerm2 的 Preferences （使用快捷键 CMD+, 可快捷打开）-\u0026gt; Color 下的 \u0026ldquo;Color Presets\u0026rdquo; 中新增一条颜色面板选项，即 \u0026ldquo;material-design-color\u0026rdquo;。选中确认即可将 iTerm2 默认的颜色面板修改为 \u0026ldquo;material-design-color\u0026rdquo;。\n上面的命令是通过 curl 下载配色文件，通过 open 打开直接安装。其实，稍微麻烦一点做法是，可通过设置面板 import 导入下载的后缀为 itermcolors 的文件。\n如下所示：\n想有更多的选择，再安装两个配色方案：\n安装 Snazzy：\ncurl -Ls https://raw.githubusercontent.com/sindresorhus/iterm2-snazzy/main/Snazzy.itermcolors \u0026gt; /tmp/Snazzy.itermcolors \u0026amp;\u0026amp; open /tmp/Snazzy.itermcolors 安装 Dracula:\ncurl -Ls https://raw.githubusercontent.com/dracula/iterm/master/Dracula.itermcolors \u0026gt; /tmp/Dracula.itermcolors \u0026amp;\u0026amp; open /tmp/Dracula.itermcolors 查看 Color Presets 面板，如下所示：\n选择一款你钟爱的配色，保存。\n如果还没有满意的，可以从 Iterm2-color-schemes 查找更多配色方案\n主题切换效果，如下所示：\n如何使用 # 打开 \u0026lsquo;iTerm2\u0026rsquo;，快速使用体验一下吧。\n分屏功能 # 如下图所示，\u0026ldquo;CommandL+d\u0026rdquo; 垂直分屏，\u0026ldquo;Command+D\u0026rdquo; 水平分屏。\n当然，这个快捷键是可以配置的，因为这两个快捷键趋势不够便捷。我们打开它的快捷键配置，进入 Preferences -\u0026gt; Keys -\u0026gt; key Binds 即可开始设置快捷键键。\n其中的红色框内容可用于设置如何水平和垂直分割屏幕：\nSHIFT+COMMAND+| -\u0026gt; 水平切片 split horizontally； SHIFT+COMMAND+- -\u0026gt; 水平切片 split horizontally； 其中的蓝色框区域可用于设置 vim 风格的上下左右分屏切换：\nCOMMAND+h：向左选中 COMMAND+j：向下选中 COMMAND+k：向上选中 COMMAND+l：向右选中 对于习惯使用 vim，但不是每个任务都有打开 tmux + vim 组合的时候，这个快捷键的设置就是效率神器啊。\n分屏最大化 # 如下图所示，\u0026ldquo;Shift+Command+Enter\u0026rdquo; 分屏最大化。\n再次 \u0026ldquo;Shift+Command+Enter\u0026rdquo; 恢复分屏。\n如果你习惯于在终端工作，那么分屏功能肯定是日常使用最多的能力了吧。如上的三分屏幕效果，左边座位代码编辑区域、右上方用于调试或运行代码，下面可用于其他一些测试或者运行命令区域。\n支持搜索 # 相对于 通过 \u0026ldquo;Command+f\u0026rdquo; 开启搜索框：\niTerm2 的搜索能力更强大，可以在搜索框下拉仔细检查下它能力。诸如默认的 smartcase 模式、大小写敏感和不敏感模式、正则。可能你觉得这些不是很正常吗？俗话说，没有对比就没有伤害，如果你和系统默认的终端对比下就知道它的优秀之处了。\n其他 # iTerm2 是真彩 256 colors，这才让我们可以在 iTerm2 将 neovim 打造成媲美 vscode 的 IDE。\n真彩测试，如下所示；\n获取脚本，访问脚本地址。\nIDE 效果：\n还有，我们可以直接在 iTerm2 查看图片，静态图片和 GIF 都是支持的。\n我现在就希望有一天终端支持内置浏览器，实现我 360 度无死角不用离开终端的梦想。我知道有一些文本 browser，如 w3m、lynx、links 等等。还有 browsh 这样的利用 firefox 渲染，终端展示的图形化支持的浏览器。但体验都巨差，无法真正意义上替换浏览器。\n我的梦啊！\n高级能力 Python API # 这部分主要介绍 iTerm2 提供的 Python API，利用它，带你实现一些不一样的能力。我将演示两个案例，分别是背景图自动更换和分屏创建自动化。\n自动更换背景图 # 编程枯燥无味，想通过背景图为枯燥生活提供一些趣味。假设，我们要设计一个脚本，给终端一小时自动更好一个背景图。\n假设我的壁纸图片都在 ~/Public/images/beauties 目录下。\n实现下这个代码，如下所示：\nimport iterm2 import random import os import asyncio import sys # 替换背景图片的函数 async def change_background(session, image_path): # 获取当前会话的配置文件 profile = await session.async_get_profile() # 设置透明度，值在0（完全透明）到1（完全不透明）之间 await profile.async_set_transparency(0.2) # 设置为50%透明度 # 设置背景图像位置 await profile.async_set_background_image_location(image_path) async def main(connection): app = await iterm2.async_get_app(connection) window = app.current_terminal_window if window is None: return session = window.current_tab.current_session # 获取脚本参数提供的目录 directory = sys.argv[1] if len(sys.argv) \u0026gt; 1 else \u0026#34;.\u0026#34; if not os.path.isdir(directory): raise ValueError(f\u0026#34;Provided path {directory} is not a directory\u0026#34;) while True: # 创建无限循环以定期更换背景 # 获取目录中的所有图片文件 image_files = [ os.path.join(directory, f) for f in os.listdir(directory) if f.lower().endswith((\u0026#34;.png\u0026#34;, \u0026#34;.jpg\u0026#34;, \u0026#34;.jpeg\u0026#34;, \u0026#34;.tiff\u0026#34;, \u0026#34;.bmp\u0026#34;, \u0026#34;.gif\u0026#34;)) ] if image_files: # 随机选择一张图片 chosen_image = os.path.abspath(random.choice(image_files)) # 调用函数更改背景 await change_background(session, chosen_image) else: print(f\u0026#34;No images found in directory {directory}\u0026#34;) # 等待一个小时 await asyncio.sleep(3600) iterm2.run_until_complete(main) 代码中提供了注释说明，不熟悉 Python 直接考虑即可，命令的接收参数是存放图片的目录。\n定时能力，我们是通过 sleep(3600) 实现每小时随机更换背景图片。特别说明，不要用 crontab，因为存在环境上下文问题，crontab 无法知道 iTerm2 的存在。\n这个脚本可以放在 sh 的启动脚本中，如 bashrc、zshrc 等，这取决于你用什么 shell。我们只要将其设置为后台常驻的形式运行，\npython random-bg.py your-images-directory \u0026amp; 我假设将它设置为 1 秒换一次图片，效果如下所示：\n布局自动化 # 如果你用 tmux 的话，可能知道有些布局管理工具，如 tmuxifier 和 tmuxinator， 可自动创建布局。其实，只用 iTerm2，同样可实现这样的功能。\n因为 iTerm2 的 Python API 提供了创建分屏的接口。\n示例代码如下所示：\nimport iterm2 async def main(connection): app = await iterm2.async_get_app(connection) window = app.current_terminal_window if window is None: print(\u0026#34;No current window\u0026#34;) return session = window.current_tab.current_session # 垂直分屏 await session.async_split_pane(vertical=True) # 如果你想要水平分屏，将vertical参数设置为False # await session.async_split_pane(vertical=False) iterm2.run_until_complete(main) 重点就是那句 await session.async_split_pane(vertical=True)，执行这个角度会自动进行左右分屏，即垂直分屏。\n演示效果如下：\n假设，你想自定义一些布局的话，如前面提到这个效果，如下所示：\n我们来实现一个命令自动创建这个效果。\n需求详细描述，iTerm2 自动创建三个 pane 的布局，左边 pane 用于通过 vim 命令打开指定目录（IDE 写代码)，右边上下两个 pane 中，上面的 pane 执行 go run *.go 命令，下面的 pane 创建清空等待输入。\n编写这样一个脚本来实现这个流程，代码如下：\nimport iterm2 import sys async def main(connection): # 获取要打开的目录作为参数 directory = sys.argv[1] if len(sys.argv) \u0026gt; 1 else \u0026#34;.\u0026#34; app = await iterm2.async_get_app(connection) window = app.current_terminal_window if window is not None: # 创建左边的pane left_session = window.current_tab.current_session await left_session.async_send_text(f\u0026#34;cd {directory}\\n\u0026#34;) await left_session.async_send_text(\u0026#34;vim\\n\u0026#34;) # 创建右边的pane # 右上的pane保持空白 top_right_session = await left_session.async_split_pane(vertical=True) await top_right_session.async_send_text(\u0026#34;\\clear\\n\u0026#34;) await top_right_session.async_send_text(\u0026#34;go run *.go\\n\u0026#34;) # 在右下的pane执行go run *.go bottom_right_session = await top_right_session.async_split_pane(vertical=False) await bottom_right_session.async_send_text(\u0026#34;\\clear\\n\u0026#34;) else: print(\u0026#34;No current window\u0026#34;) iterm2.run_until_complete(main) 就不啰嗦代码逻辑了，非常简单。\n命令效果，如下所示：\nPython API 的使用就演示这两个案例。想了解它的更多能力，可直接查看它的官方文档，访问 iTemr2 Python API Documentation。\n最后 # 本文重点介绍了 iTerm2 的安装、颜色面板的配置，还有体验其核心的能力，最有趣的部分是通过 iTerm2 的 Python API 能力，实现了一些自动化任务（自动更换背景和布局管理），提高趣味性和效率。\n博文地址：终端环境：iTerm2\n","date":"2023-09-28","externalUrl":null,"permalink":"/posts/2023-09-25-install-iterm2-as-my-developing-environment/","section":"文章","summary":"视频版本，没有文章详细：\n我想大部分程序员在平时的工作中都是离不开终端，特别是如果你的系统是 MacOS 或 Linux 的话，终端的地位更是遥遥领先。\n","title":"终端环境：iTerm2","type":"posts"},{"content":"","date":"2023-09-27","externalUrl":null,"permalink":"/tags/neovim/","section":"Tags","summary":"","title":"Neovim","type":"tags"},{"content":"","date":"2023-09-27","externalUrl":null,"permalink":"/tags/vim/","section":"Tags","summary":"","title":"Vim","type":"tags"},{"content":"本文介绍，如何基于 LunarVim 搭建不同编程语言的 Neovim IDE 开发环境。\n前言 # 本文将用几行命令快速安装 Neovim IDE，完成不同编程语言的环境搭建。尽量不涉及到自定义配置，将完全基于 LunarVim 作者维护的配置实现。\n两个 Github 核心仓库，分别是：\nlunarvim/lunarvim，是 LunarVim 的核心仓库，集成配置 IDE 所需的核心能力； lunarvim/starter.lvim，这个仓库是 Lunarvim 针对不同编程语言的配置实现； starter.lvim 以分支形式保不同语言的配置，具体自行查看仓库。\n为了测试方便，介绍 LunarVim 提供的一个能力，通过 Lunarvim 通过 LUNARVIM_CONFIG_DIR 变量决定配置文件目录。\n接下来的测试，我会将不同语言的配置，放到不同的目录中。\n如果希望一个配置支持大部分语言，则要将这些配置合并，进行配置自定义，对 Lunarvim 的 configlua 代码进行大的改动，不易维护。\n安装 # LunarVim 提供了安装脚本，使用如下命令安装即可。\nLV_BRANCH=\u0026#39;release-1.3/neovim-0.9\u0026#39; bash \u0026lt;(curl -s https://raw.githubusercontent.com/LunarVim/LunarVim/release-1.3/neovim-0.9/utils/installer/install.sh) 安装过程中要下载一些依赖，如 pynvim，cargo 之类的，如果已经安装可选择 no。\n注：部分语言环境和命令要提前安装。如 python，make, git 等\n如果要 dev icon 支持，安装 Nerd 字体，macOS 安装命令如下：\nbrew tab homebrew/cask-fonts brew install --cask font-hack-nerd-font 安装成功，将终端字体更新为 Hack Nerd Font 相关字体。\n如下所示，以 iTerm2 为例：\n现在执行 lvim，将看到 lazy.nvim 开始自动下载插件，如下是效果图：\n初步体验 # 体验下 Lunarvim 默认配置，与语言无关的能力。\n目录浏览 # 使用 \u0026ldquo;\u0026lt;空格\u0026gt;+e\u0026rdquo; 显示文件树，再次 \u0026ldquo;\u0026lt;空格\u0026gt;+e\u0026rdquo; 关闭，效果如下：\n查找文件 # 使用 “\u0026lt;空格\u0026gt;+f”，查看文件，如下：\n查找符号 # 输入 \u0026ldquo;\u0026lt;空格\u0026gt;+lS\u0026rdquo;，全部符号搜索，如下：\n重构变量名 # 输入 \u0026ldquo;\u0026lt;空格\u0026gt;+lr\u0026rdquo;，将 \u0026lsquo;anyMethods\u0026rsquo; 更新为 \u0026lsquo;anyMethodList\u0026rsquo;，如下：\n类型引用查找 # 输入 \u0026ldquo;gr\u0026rdquo;，查找 \u0026ldquo;RouterGroup\u0026rdquo; 被引用的地方。\n定义跳转 # 输入 \u0026ldquo;gd\u0026rdquo;，跳转到 \u0026ldquo;RouterGroup\u0026rdquo; 定义。\nGit 支持 # 内置 lazygit 处理复杂事务，同时内置对部分 ShortCut 和简单的 Git 命令的进行了绑定。\nLunarVim 默认支持的能力还是挺丰富的。\n详细内容可查看它的文档，或者 \u0026ldquo;\u0026lt;空格\u0026gt;\u0026rdquo; 显示 whch-key 菜单，自行探索。深入了解的话，最好是阅读 Lunarvim 的 lua 配置源码。如果能独立配置 Neovim，再看它的源码会有更好的体悟。\n配置 # 前面已经介绍 LunarVim 和 starter.lvim 的关系。实操的话，如何配置呢？\nGolang # 首先，以 go-ide 为例，使用如下命令启动 lvim：\nalias lv-go=\u0026#34;LUNARVIM_CONFIG_DIR=~/.config/lvims/golang lvim \u0026#34; 将 starter.lvim 的 go-ide 分支下载到 ~/.config/lvims/golang/ 目录下。\ngit clone https://github.com/Lunarvim/starter.lvim --branch go-ide ~/.config/lvims/golang 下载完成，记得在原有的 config.lua 中的新增一行配置，如下所示;\nlvim.format_on_save.enabled = true 实现保存后的通过 goimports 和 gofumpt 实现代码的自动格式化。\n运行 lv-go 开启 Go 项目。\n首次进入，通过 Mason 安装一些 Go 相关的工具，如下：\n:MasonInstall gopls golangci-lint-langserver delve goimports gofumpt gomodifytags gotests impl 体验下效果：\n错误提示，自动补全能力，都已经支持的很棒了。\nPython # 配置 python-ide，使用如下命令启动 lvim：\nalias lv-py=\u0026#34;LUNARVIM_CONFIG_DIR=~/.config/lvims/python lvim \u0026#34; 将 starter.lvim 的 go-ide 分支下载到 ~/.config/lvims/python/ 目录下。\ngit clone https://github.com/Lunarvim/starter.lvim --branch python-ide ~/.config/lvims/python 运行 lv-py 开启 Python 项目。效果图如下：\n其他语言 IDE 的安装就不详细说了，类似步骤。另外，starter.lvim 支持编程语言很多，按需要取用。\n最后说明 # 本文只是快速使用体验教程，如希望更深入了解，可自行查看配置。诸如了解它的快捷键如何使用，只有直接看配置才能知道。\n另外，这里的配置除了支持 lsp，language server，基本也都支持 dap，即调试能力。\n由于，这些配置的使用细则都没有提供文档，而且既然已经用 neovim 了。至少还是要能看懂和写写配置的，不然是没有办法用好 Neovim 的，更谈不上，个人偏好的配置自定义。\n","date":"2023-09-27","externalUrl":null,"permalink":"/posts/2023-09-27-start-an-ide-using-lunarvim/","section":"文章","summary":"本文介绍，如何基于 LunarVim 搭建不同编程语言的 Neovim IDE 开发环境。\n前言 # 本文将用几行命令快速安装 Neovim IDE，完成不同编程语言的环境搭建。尽量不涉及到自定义配置，将完全基于 LunarVim 作者维护的配置实现。\n","title":"基于 LunarVim 搭建不同编程语言 IDE","type":"posts"},{"content":"本文将介绍如何实现 Neovim 的配置隔离，实现不同编程语言使用不同的编辑器配置。\n背景说明 # 近段时间，一直在学习如何高效使用 Neovim。不断配置的过程中想到，Neovim 是否支持配置隔离，不同用途不同配置。最直接的体现，我希望把 Python 和 Golang 的编辑开发环境的配置隔离。\n类似如下效果：\nnvim-golang main.go nvim-python main.py nvim-cpp main.cpp 提到这，不由地想到了 Jetbrain 全家桶，针对不同编程语言开发了各自的 IDE，如 goland，pycharm，webstorm、clion 等。猜测原因，或许是为了多赚钱，另一方面，不同语言一定有个性化配置，隔离能减少耦合。\n如果是因为想搭建某种语言的编程环境，推荐阅读：基于 LunarVim 搭建不同编程语言 IDE\n如何实现呢？进入正题吧。\n几年前，写过一篇关于 \u0026ldquo;Golang 多环境管理 GVM\u0026rdquo; 的文章。本质上，要实现这种多环境隔离，一般都是通过环境变量实现。查了些资料，Neovim 其实也不例外。\n方案 1：基于 XDG 配置 # Neovim 的目录遵循 XDG 目录规范。具体是什么意思呢？\nXDG 本质是一套规范，定义了一组环境变量，用于说明应用程序储存信息目录的一套标准。熟悉 Linux 的朋友应该了解，我们以往一直习惯于把应用的配置以 .xxx 的形式放在用户的 $HOME 目录，导致 $HOME 下的点隐藏文件泛滥，而这套规范的出现，使我们轻易实现目标。\n就以 Neovim 为例：\nNeovim 的配置文件存放默认存放在 $XDG_CONFIG_HOME/nvim，数据目录默认在 $XDG_DATA_HOME/nvim，状态数据目录默认在 $XDG_STATE_HOME/nvim，缓存数据目录默认在 $XDG_CACHE_HOME/nvim。\n通过修改 XDG 环境变量，即可实现环境隔离。\n如下 nvim-golang 的启动脚本：\n#!/bin/bash export XDG_CONFIG_HOME=$HOME/.config/nvim-golang export XDG_DATA_HOME=$HOME/.local/share/nvim-golang export XDG_STATE_HOME=$HOME/.local/state/nvim-golang export XDG_CACHE_HOME=$HOME/.cache/nvim-golang nvim $@ 注：XDG 变量的修改范围只能在命令脚本内，不可影响 XDG 全局配置。\n现在的 nvim 目录情况：\n$HOME/.config/nvim --\u0026gt; $HOME/.config/nvim-golang/nvim $HOME/.local/share/nvim --\u0026gt; $HOME/.local/share/nvim-golang/nvim $HOME/.local/state/nvim --\u0026gt; $HOME/.local/state/nvim-golang/nvim $HOME/.cache/nvim --\u0026gt; $HOME/.cache/nvim-golang/nvim 假设，配置一个 nvim-golang 的编辑器，以 LunarVim 的作者提供的 nvim-basic-ide 配置为例。\n下载配置到指定目录：\ngit clone https://github.com/LunarVim/nvim-basic-ide.git $HOME/.config/nvim-golang/nvim 假设，上述启动脚本文件名为 nvim-golang，执行脚本 nvim-golang main.go 编辑文件，初次进入会下载安装 nvim-basic-ide 中的插件。\n方案二：NVIM_APPNAME # 或许是官方也知道了配置隔离的价值。从 Neovim 0.9.0 开始，Neovim 默认支持一个环境变量 NVIM_APPNAME 隔离环境配置。\n以 nvim-python 为例，我们只需通过 NVIM_APPNAME=nvim-python nvim main.py，即可改变 Nvim 的目录。\n此时目录：\n$HOME/.config/nvim --\u0026gt; $HOME/.config/nvim-golang $HOME/.local/share/nvim --\u0026gt; $HOME/.local/share/nvim-golang $HOME/.local/state/nvim --\u0026gt; $HOME/.local/state/nvim-golang $HOME/.cache/nvim --\u0026gt; $HOME/.cache/nvim-golang 相对于方案一而言，无需在 nvim-python 追加子目录 nvim ，启动脚本 nvim-python 的内容只要一行代码：\nNVIM_APPNAME=nvim-python nvim @ 或者更简洁的实现，通过 alias 别名：\nalias nvim-python=\u0026#34;NVIM_APPNAME=nvim-python nvim\u0026#34; 将 nvim-basic-ide 下载到 ~/.config/nvim-python 下：\ngit clone https://github.com/LunarVim/nvim-basic-ide.git $HOME/.config/nvim-python 与方案一的效果一致，后面针对 python 环境的定制就可以都在这个环境下进行了。\n方案三：混合使用 # 方案一和二都有个小缺点，首先一方案 nvim-golang 总要追加一个子目录，而方案二呢？在 ~/.config 下会创建多个 nvim 相关配置。\n那么，有没有一种方法，能将 nvim 的不同配置集合到一个目录之下，统一管理，效果以类似于:\n$HOME/ - .config/ ... - nvims - golang - python ... 简单！两者结合即可：\n首先，将 XDG 的配置统一下移到 nvms 下，什么意思呢？如原来的 XDG_CONFIG_HOME 是 $HOME/.config/，现在是 $HOME/.config/nvims，其他变量类似：\n脚本如下：\nexport XDG_CONFIG_HOME=$HOME/.config/nvims export XDG_DATA_HOME=$HOME/.local/share/nvims export XDG_STATE_HOME=$HOME/.local/state/nvims export XDG_CACHE_HOME=$HOME/.cache/nvims 接着，通过 NVIM_APPNAME 作为 nvims 的子目录。如 nvim-golang 的命令实现：\nNVIM_APPNAME=golang nvim $@ 现在只要将 nvim-basic-ide 的配置下载在 $HOME/.config/nvims/golang 下。\ngit clone https://github.com/LunarVim/nvim-basic-ide.git $HOME/.config/nvims/golang 搞定！\n完整 nvim-golang 脚本如下：\nexport XDG_CONFIG_HOME=$HOME/.config/nvims export XDG_DATA_HOME=$HOME/.local/share/nvims export XDG_STATE_HOME=$HOME/.local/state/nvims export XDG_CACHE_HOME=$HOME/.cache/nvims NVIM_APPNAME=golang nvim $@ 启动 nvim-golang 查看效果。\n插件复用 # 最后，还有一个问题，即配置的过程中，可能已经有朋友发现，插件会重复多次下载。这是因为，默认插件在下载位置位于 $XDG_DATA_HOME/$NVIM_APPNAME 下，故而不同的 $XDG_DATA_HOME 或 NVIM_APPNAME 都会导致下载插件所在位置的不同。\n但插件管理器一般是支持配置插件下载路径的，以 lazy 为例，nvim-basic-ide 中 init.lua 中的代码如下：\nlocal lazypath = vim.fn.stdpath \u0026#34;data\u0026#34; .. \u0026#34;/lazy/lazy.nvim\u0026#34; if not vim.loop.fs_stat(lazypath) then vim.fn.system { \u0026#34;git\u0026#34;, \u0026#34;clone\u0026#34;, \u0026#34;--filter=blob:none\u0026#34;, \u0026#34;https://github.com/folke/lazy.nvim.git\u0026#34;, \u0026#34;--branch=stable\u0026#34;, -- latest stable release lazypath, } end vim.opt.rtp:prepend(lazypath) -- ... require(\u0026#34;lazy\u0026#34;).setup(\u0026#34;user\u0026#34;, { root = vim.fn.stdpath(\u0026#34;data\u0026#34;) .. \u0026#34;/../lazy\u0026#34;, ... }) 通过修改 lazypath 可以和 setup 时设置 root 变量实现多环境共享插件。\n那么，插件的问题解决了。\n尝试实操下。\n首先，下载 nvim-basic-ide 到 nvims 的 ~/.config/nvims/golang/ 目录。\ngit clone https://github.com/LunarVim/nvim-basic-ide.git $HOME/.config/nvims/golang 将 $HOME/.config/nvims/golang/lua/Lazy.lua 中的 lazypath 更新为：\nlocal lazypath = vim.fn.stdpath \u0026#34;data\u0026#34; .. \u0026#34;/../lazy/lazy.nvim\u0026#34; 同样方法配置 nvim-python，将 nvim-golang 配置拷贝一份，即 ~/.config/nvims/golang 拷贝到 ~/.config/nvims/python。同时，将 nvim-golang 启动脚本拷贝一份为 nvim-python，NVIM_APPNAME 修改为 python。\n如下所示：\n#!/bin/bash export XDG_CONFIG_HOME=$HOME/.config/nvims export XDG_DATA_HOME=$HOME/.local/share/nvims export XDG_STATE_HOME=$HOME/.local/state/nvims export XDG_CACHE_HOME=$HOME/.cache/nvims NVIM_APPNAME=python nvim $@ 测试效果，启动 nvim-python 已无需重新加载插件。\n代码复用 # 另外一个优化点，是否可以将 nvim-basic-ide 的能力作为基础共享，在此基础上配置 goalng 和 python 各自的配置。\n额？好像 LunarVim 支持这个能力，它已经把用户配置从核心 IDE 基础配置中摘除出来了。\n它的基本思路是在核心配置中引入用户定义配置。\n简单来说就是主配置目录下的 init.lua 通过一个变量 LUNARVIM_CONFIG_DIR 实现 require 用户配置 config.lua 实现多环境。实现代码在 bootstrap.lua 文件中。\n以 golang 环境为例，一个别名搞定：\nalias lvim-golang=\u0026#34;LUNARVIM_CONFIG_DIR=${HOME}.config/lvims/golang lvim @\u0026#34; 我们在 ~/.config/lvims/golang/config.lua 实现针对 Golang 的自定义配置。\n有兴趣的话，可以实现下，这套思路也不错。\n特别补充：这个思路可以完全舍弃前面提到的几种配置隔离方案，不需要修改 XDG 变量或者 NVIM_APPNAME。它通过唯一的配置入口 require 不同环境（LUNARVIM_CONFIG_DIR）下的 config.lua，从而实现复用核心配置，同时可以自定义个性化需求。\nLunarvim 针对不同编程语言的配置方法，可查看 基于 LunarVim 搭建不同编程语言 IDE\n最后 # 关于配置隔离的思路，到此就已经全部介绍完了。\n这个思路不仅仅可用于多语言环境的隔离，对于市面上有很多别人维护的配置，利用这个思路都能快速下载体验。\n如下是市面上流行的一些集成配置：\nkickstart，所有配置一个文件搞定； neovim-basic-ide: 本文测试所用，LunarVim 开发者维护的一个基础配置； LazyVim: 提供 IDE 所需的基本配置； 还有 NvChad 、AstroNvim 等，甚至一些个人也会开源自己的配置； 有兴趣都可以尝试下有什么差异。\n博文地址：Neovim 配置隔离-实现多语言环境支持\n","date":"2023-09-25","externalUrl":null,"permalink":"/posts/2023-09-25-running-multiple-nvims-in-your-computer/","section":"文章","summary":"本文将介绍如何实现 Neovim 的配置隔离，实现不同编程语言使用不同的编辑器配置。\n背景说明 # 近段时间，一直在学习如何高效使用 Neovim。不断配置的过程中想到，Neovim 是否支持配置隔离，不同用途不同配置。最直接的体现，我希望把 Python 和 Golang 的编辑开发环境的配置隔离。\n","title":"Neovim 配置隔离-实现多语言环境支持","type":"posts"},{"content":"我们知道，Vim 支持配置是否显示行号，对这个行号认知，我们一般指的是绝对行号。其实 Vim 支持配置两种行号模式：number（绝对行号） 和 relativenumber（相对行号）。\n今天，基于 vim 行号介绍一个提升其使用效率的小技巧，混合使用 number 和 relativenumber。\n绝对行号 number # 绝对行号 number，我们基本都熟悉怎么使用。效果图如下所示：\n通过 set number 显示行号。默认开启的话，配置到 Vim 配置文件中即可。\n其他命令：\n\u0026#34; 显示行号 set nu \u0026#34; set number 的缩写形式 \u0026#34; 隐藏行号 set nonumber \u0026#34; 无缩写 set nonu \u0026#34; 缩写形式 基于行号 number，实现的一些快捷操作，如：\n基于行的快速跳转 10G 或 :10，快速跳转到第 10 行； 粘贴指定范围文本 :10,20y 或删除 :10,20d； 替换指定范围文本 :10,20s/hello/world/g； 注：set numberwidth=4 可配置行号所在的列的默认宽度为 4，如果行号数值达到 5 位数，将会自动扩展到 5 位。另外说明，不同于 Vim 的默认值是 2，Neovim 的默认宽度也是 4。\n相对行号 relativenumber # 在谈相对行号前，其实 Vim 另外一种行间跳转方式：基于相对位置，可使用如 10k 或 10l 向上向下快速跳转，是一种适合在屏幕范围快速跳转的方式。\n但它缺点是，要数这个相对位置，如上图中，假设要从 vim.o.shiftwidth = true 跳转到 vim.o.autoindent=true ，可使用 :13 或者通过 6l（向下跳转 6 行）实现，第二种方式快速计算相对行号，使用 13-7 = 6，有一个计算过程，并不方便。\n如何解决？\n我们可引入 relativenumber，即相对行号，效果图如下：\n当前位置的相对行号是 0，而上下行号以此为基准递增，是动态变化的数值。基于相对行号，就能在通过相对位置跳转到窗口范围内任意一行。\n相关配置命令：\nset rnu \u0026#34; set relativenumber 的缩写形式\u0026#34; \u0026#34; 隐藏相对行号 set norelativenumber set nornu \u0026#34; set norelativenumber 的缩写形式 特别说明：如果 number 和 relativenumber 同时开启的情况下，当前所在行显示绝对行号，而其他行则显示相对行号。\n按模式切换 # 有人提出一种将 number 和 relativenumber 结合使用的方式：在 Insert 模式下使用绝对行号，在其他模式下使用相对行号。猜测原因是，相对行号主要还是在非 Insert 模式下使用。\n这样的效果就能同时兼顾 number 和 relativenumber。\n配置 vimrc，语句如下：\naugroup numbertoggle autocmd! autocmd BufEnter,FocusGained,InsertLeave,WinEnter * if \u0026amp;nu \u0026amp;\u0026amp; mode() != \u0026#34;i\u0026#34; | set rnu | endif autocmd BufLeave,FocusLost,InsertEnter,WinLeave * if \u0026amp;nu | set nornu | endif augroup END 或者\n如果是 Neovim，引入插件：nvim-numbertoggle 亦可。\n简洁的源码，如下所示：\nlocal augroup = vim.api.nvim_create_augroup(\u0026#34;numbertoggle\u0026#34;, {}) vim.api.nvim_create_autocmd({ \u0026#34;BufEnter\u0026#34;, \u0026#34;FocusGained\u0026#34;, \u0026#34;InsertLeave\u0026#34;, \u0026#34;CmdlineLeave\u0026#34;, \u0026#34;WinEnter\u0026#34; }, { pattern = \u0026#34;*\u0026#34;, group = augroup, callback = function() if vim.o.nu and vim.api.nvim_get_mode().mode ~= \u0026#34;i\u0026#34; then vim.opt.relativenumber = true end end, }) vim.api.nvim_create_autocmd({ \u0026#34;BufLeave\u0026#34;, \u0026#34;FocusLost\u0026#34;, \u0026#34;InsertEnter\u0026#34;, \u0026#34;CmdlineEnter\u0026#34;, \u0026#34;WinLeave\u0026#34; }, { pattern = \u0026#34;*\u0026#34;, group = augroup, callback = function() if vim.o.nu then vim.opt.relativenumber = false vim.cmd \u0026#34;redraw\u0026#34; end end, }) 注意下，这里加了个限制，只有在启动 number 的情况下，才会按模式切换 relativenumber。\n最后 # 实话实说，虽然这方式看起来挺炫酷，但我一直不太习惯。毕竟，窗口范围内，我也可以使用绝对行号。或许是我没有养成良好的习惯。这次决定尝试下，开启这个配置，锻炼下自己的思维和手指。\n博文地址：Vim 小技巧：高效利用 vim 的行号\n","date":"2023-09-25","externalUrl":null,"permalink":"/posts/2023-09-25-vim-tips-how-to-use-number/","section":"文章","summary":"我们知道，Vim 支持配置是否显示行号，对这个行号认知，我们一般指的是绝对行号。其实 Vim 支持配置两种行号模式：number（绝对行号） 和 relativenumber（相对行号）。\n","title":"Vim 小技巧：高效利用 vim 的行号","type":"posts"},{"content":"","date":"2023-09-18","externalUrl":null,"permalink":"/tags/bash/","section":"Tags","summary":"","title":"Bash","type":"tags"},{"content":"","date":"2023-09-18","externalUrl":null,"permalink":"/tags/shell/","section":"Tags","summary":"","title":"Shell","type":"tags"},{"content":"我们知道，在所有的 Linux/Unix 中 shell，Bash 是最流行的，它是多数 Linux 发行版的默认 shell。除了 bazh，zsh 是另外一款非常流行的 shell。它功能更强大，而且还是 macOS 中的默认 Shell。\nzsh 为什么如此受欢迎？我们是否应该使用它呢？\n什么是 zsh？ # “Z shell” 最初是由 Paul Falstad 在普林斯顿大学就读时开发。\nZsh 整合了绝大多数主流 Shell 中的功能，如 Bourne-Again Shell (Bash)、Korn Shell (ksh)、C-shell (csh) 和 tcsh。故而，zsh 与这些主流 shell 都有一定程度的兼容，是其更受用户欢迎。\n如今，Zsh 俨然已经是一个庞大的开源项目（非 Paul Falstad 维护），拥有一个有大量用户和贡献者的社区。而且，自 2019 年以来，它成为了 Apple macOS 的默认 Shell。\nbash vs zsh # 这两个项目都还在积极开发中。这使它们在功能上越发接近，但差异不可能完全消除。默认，zsh 更强大且更容易自定义，而某些功能， Bash 需要一些额外的脚本（插件）才能实现。\nzsh 优于 Bash 的主要功能是：\nzsh 的补全能力强大，bash 的 Tab 补全是从头匹配，如 mn 匹配 mnt，而非 findmn，而 zsh 可同时匹配 mnt 和 findmn；\nzsh 的命令行历史是在 terminal 间共享，结合自动补全，进一步增强了用户体验；\nzsh 还提供自动纠错能力，如果你输入太快，它能智能给你一个可能正确的建议；\nzsh 的配置能力更强，支持构建更精美的提示主题；\nzsh 参数的扩展能力比 Bash 更强大；\nzsh 有大量插件、主题、框架，如 oh-my-zsh 框架，能助你快速配置出一个强大终端；\n我们是否要用 zsh？ # zsh 已被证明是一个功能强大高效、高可定制的 shell，它使我们能轻松拥有一个体验丝滑的 CLI 终端。\n如果你一个时常与命令行为伴的开发人员，那 zsh 绝对是一个不错选择。再者，如果你愿意投入更多时间去探索它，将会发现更多惊喜。\n我该放弃 bash 吗？ # bash 不会消失，这是事实！它是大多数 Linux 发行版中的标准 shell，这意味着你在世界上的大多数 server、container、vps 和云实例都能找到它。\n另外，对于 shell 脚本，除非有一些特殊需求，否则最好编写标准的 bash script。\n你可能主要使用 Zsh，但不要认为你不会再接触 bash，至少，bash 脚本在未来的几年仍是一个可靠的选择。\n为什么 zsh 是 macOS 的默认 shell？ # 这个问题的答案，除了与 zsh 的强大特性有关，还一个重要因素是关于 License。\n切换到 zsh 前，MacOS 多年以来内置的一直是过时版本的 bash （v3.2, 2007 年发布），作为默认 shell，这是 bash 以 GPLv2 授权的最后一个版本。 3.2 之后，新版本的 Bash 均是 GPLv3，这一许可对于 Apple 似乎无法接受。\n终于在 2019 年，Apple 转向了 MIT License 的 zsh。\n结论 # zsh 能增强你的 CLI 终端使用体验，而且，只需简单安装即可使其大放异彩。\n但，不得不说，bash 现在依然是 shell 脚本的不错选择，熟悉标准 bash 安装对还是会非常有用。它涉及 DevOps、系统管理、云计算和容器，因为大多数 Linux 发行版的默认 shell 还是 bash。\n请安装开始你的探索吧。我相信，你会发现它的强大。或许，最终它会成为你的默认 shell。\n博文地址：什么是 zsh？我是否应该使用 zsh，译：What is Zsh? Should I Use it?\n","date":"2023-09-18","externalUrl":null,"permalink":"/posts/2023-09-16-what-how-to-use-zsh/","section":"文章","summary":"我们知道，在所有的 Linux/Unix 中 shell，Bash 是最流行的，它是多数 Linux 发行版的默认 shell。除了 bazh，zsh 是另外一款非常流行的 shell。它功能更强大，而且还是 macOS 中的默认 Shell。\n","title":"什么是 zsh？我是否应该使用 zsh","type":"posts"},{"content":"本文将介绍如何使用 zsh 来提升命令行的操作效率。\n你是否每天都在与命令行打交道？\n如果答案是 \u0026ldquo;Yes\u0026rdquo;，那你肯定想拥有一个强大可定制的 Shell。 而 zsh 就是为这个目标而生，它运行于诸如 Linux 、MacOS 等类 Unix 系统下，可替换默认的 bash。\nzsh 是什么？ # zsh，或 Z Shell，是一个 Unix-Like 系统（如 macOS 或 Linux）下的 shell 命令行解释器。\n它支持强大的自动补全能力，拥有丰富的插件，具有高可定制性，而且与 bash 充分兼容。虽然，它与 bash 相比，能力更加强大，但是它却依然比 bash 更快。\n再者，相较于 bash，zsh 现在社区更加活跃，是一个还在成长中的项目。\nzsh 的优势 # zsh 和 bash 都是非常流行的 Unix-like shell，它们有着很多相似的功能特性。但相对于 bash，zsh 有这些差异化优势：\n更优秀的命令行补全能力； 配置化能力更强； 更现代的语法； 改进的错误报告； 模拟 bash； 用户社区不断壮大，更新频繁； 体验更好的按键 支持vi模式等 安装 zsh # Linux 系统下的安装非常简单。\nDebian:\nsudo apt install zsh Arch Linux:\nsudo pacman -S zsh Fedora\nsudo dnf install zsh 译注：从 2019 后，macOS 的默认 shell 从 bash 更新到了 zsh，无需安装，以往版本可通过 brew install zsh 安装。推荐阅读：[什么是 zsh？我该使用 zsh 吗？]\n设置zsh为默认shell # 成功安装之后，可将 zsh 设置为默认 shell。执行如下命令：\nchsh -s /bin/zsh 输入密码确认！系统默认 shell 即修改为 zsh。zsh 的配置文件位于 ~/.zshrc，开始定制专属你的 zsh 吧。\n如何使用 # 如何充分使用 zsh？开始我们的一步一步配置吧。\n1. 配置文件 # 让我们一起看看 zsh 配置文件吧。\n.zshrc 或者 ~/.config/zsh/.zshrc，它是在每次启动 shell 都会运行的文件； .zprofile，登录你的系统时运行，与 .profile 类似； .zlogin，与 .zprofile 类似，唯一区别在于 .zlogin 运行在 .zshrc 之后； .zlogout，退出登录终端时运行。它用于在退出登录时保存日志，或执行其他配置命令。但实际上，或许 cronjob 更适合这类任务； .zshenv，用于设置环境变量； 注意，以上所有的文件都有一个系统级别的对应文件，位于 /etc/zsh*，如 .zshrc 对应于 /etc/zshrc。通常，不同的 Linux 发行版会有自己的专属配置。\n2. 定制 .zshrc # 开始定制你的 .zshrc。路径可能位于 $HOME 或 $HOME/.config/zsh/。\n首先，让我们设置一个命令行提示。\nautoload -U colors \u0026amp;\u0026amp; colors PS1=\u0026#34;%B%{$fg[red]%}[%{$fg[yellow]%}%n%{$fg[green]%}@%{$fg[blue]%}%M %{$fg[magenta]%}%~%{$fg[red]%}]%{$reset_color%}$%b \u0026#34; 将 PS1 改造和 bash 有些许不同。虽然，这个 PS1 看起来依然很复杂，但比起来 bash 的方式还是更容易些。\n保存 .zshrc，重启打开 zsh 查看，如下图所示：\n配置自动补全插件:\n# Basic auto/tab complete autoload -U compinit zstyle \u0026#39;:completion:*\u0026#39; menu select zmodload zsh/complist setopt extendedglob _comp_options+=(globdots) 解释下：\n行 1：为 zsh 启用自动补全； 行 2：启用菜单的完成选项并选择（Tab将弹出菜单，回车将选择或完成命令）； 行 3：加载 zsh complist（完成脚本）； 行 4：启用 ** 通配符（通配符匹配任何文件/目录） 行 5：启用隐藏文件的自动完成功能 保存文件，执行 ls 命令，输出如下：\n3. 安装框架 # 市面上有很多由于管理 zsh 配置的框架，如 Oh-my-zsh、Prezto、Zinit 和 Antigen。\n其中，Oh My Zsh 深受用户欢迎。它配备了许多默认功能，改善您的命令行体验，如自动完成、插件、主题、语法高亮、alias 别名、自定义提示和历史命令管理。\n安装 oh-my-zsh # 使用 curl 安装 oh-my-zsh，确保你的系统已经成功安装了 curl。\nsudo apt install curl #Ubuntu / Debian sudo pacman -S curl #Arch Linux sudo dnf install curl #Fedora 下载安装 oh-my-zsh 脚本：\nsh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; 如上命令将安装 oh-my-zsh，并将现有 .zshrc 保存为 .zshrc.pre-oh-my-zsh。\n安装成功后，oh-my-zsh 会覆盖你的自定义提示，效果如下所示：\n启用插件 # 安装成功后，$HOME 目录下将会有一个名为 .oh-my-zsh 目录。zsh 的内置插件都在 ~/.oh-my-zsh/plugins。\n查看 oh-my-zsh 提供的插件，输入：\n$ cd ~/.oh-my-zsh/plugins/ $ ls 启用插件也很简单，编辑 ~/.zshrc, 将插件名称追加到 plugins=() 中即可。\n示例如下：\nplugins=(golang git autocd) Tab 补全 # 安装配置 oh-my-zsh 成功之后，tab 补全已经启用。如下效果：\n提示主题 # oh-my-zsh 的提示主题可提升终端视觉体验，如方便地查看当前所在目录、用户名、主机名等。这些通过内置插件即可实现。\n那么，如何启用 oh-my-zsh 提示主题？\n首先，你可在 ~/.oh-my-zsh/themes/ 目录或 GitHub 主题 页面上查看你想配置的主题。假设，确认选择名为 simple 主题，编辑 .zshrc，设置 ZSH_THEME 变量。\n配置如下所示：\nZSH_THEME=\u0026#34;simple\u0026#34; 重启打开一个新的终端，将会有提示信息的主题已经变了。\n其他主题推荐：powerlevel10k 和 oh-my-posh\n译注：本人在用的就是 powerlevel10k 主题。但它不是内置主题，要单独安装。\n语法高亮 # zsh 语法高亮，广泛使用的插件之一是 fast-syntax-highlighting。但它并非是 oh-my-zsh 内置插件，因为要手动安装。\n安装命令：\ngit clone https://github.com/zdharma-continuum/fast-syntax-highlighting.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/fast-syntax-highlighting 配置到 .zshrc 的插件列表，如下所示：\nplugins=(golang git autocd fast-syntax-highlighting) 重新打开终端，查看：\n译注：zsh-syntax-highlighting 似乎比 fast-syntax-highlighting 受欢迎，一个站点提供对比信息，访问：fast-syntax-highlighting vs zsh-syntax-highlighting。\nvim 模式 # 启用 vim 模式，编辑.zshrc，如下：\nbindkey -v 如上配置即可启用 vi，在终端中使用 vi 键，用 ESC 进入 vi Normal 模式，便可使用 vim 键进行导航和操作文本。\n在 vi 命令和插入模式下，绑定 Ctrl+e 实现 $EDITOR 编辑当前命令：\nbindkey -M viins \u0026#39;^e\u0026#39; edit-command-line bindkey -M vicmd \u0026#39;^e\u0026#39; edit-command-line 译注：作者这里介绍如何自定义 vi-mode，但其实 oh-my-zsh 内置了 vi-mode 插件，阅读 readme: vi-mode plugin 了解它更多功能。github 还有其他开源的 zsh-vi-mode 插件，没有去体验，猜测比内置的功能更强大。\n历史记录 # 在开始 history 命令前，我们先了解下 .zshrc 中与 history 相关的一些变量和选项。\nHISTSIZE=10000 SAVEHIST=10000 setopt appendhistory setopt INC_APPEND_HISTORY setopt SHARE_HISTORY 说明如下：\n行 1：设置保留历史记录最大行数； 行 2：设置文件中历史记录最大行数； 行 3：使用多个 zsh 时，追加到历史文件而不是替换； 行 4: 直接追加历史记录而非等待 shell 退出； 行 5：允许使用不同终端输入的命令；如果同时使用多个终端，这个配置非常有用； 如何输出历史记录？使用 history 命令即可，如下所示：\nhistory 注意，它不会保存任何只是输入但未执行的命令。\n输出最后 10 行命令：\nhistory -10 显示大于等于 20 行的命令。\nhistory +20 up 方向键，可用于查看上一个命令，down 方向键可用于查看下一个命令。\n! 是在历史命令记录查看上，有特定的能力，可协助快速使用历史命令。\n让我们看一些例子：\n!!\u0026lt;Press Tab\u0026gt; 如果在 !! 后按 Tab，将自动显示最后一个命令。特别是一些要超级权限才能执行的命令，忘记输入 sudo，可通过这种方式快速补齐。\n示例如下：\napt update # Does not work, it needs sudo sudo !!\u0026lt;Press tab\u0026gt; 按 Tab 后将自动将 apt update 补齐到 sudo ，实现快速输入。\n如果将 ! 与 结合，将也会有不一样的体验。\n!\u0026lt;number\u0026gt; \u0026lt;press tab\u0026gt; 按下 Tab 后，输入会被会自动补全为历史记录中对应编号的命令。\n命令补全 # 任何命令，如想实现 zsh 自动补全，则必须提供补全规则脚本。zsh 默认的补全规则文件存放在 /usr/share/zsh/site-functions/ 下。如某命令无法自动补全，尝试 google 搜索该命令的补全脚本，并将其放在该目录下。\n补全目录的具体位置以 $fpath 为准，不同系统发行版本可能有所不同。\necho $fpath 你也可以自定义补全目录位置，将其追加到这个变量之后即可。\n使用 FzF # Fzf 是一个模糊匹配查找器，可与 zsh 完美配合。通过 key-binding 或使用 fzf-tab 插件实现命令或目录的补全。\n让我们开始安装。\n首先，这个插件要在 zshrc 的最后加载。因此，我们无法用 oh-my-zsh 来管理这个插件。\n插件下载，将其 clone 到 ~/.config/zsh，命令如下：\ngit clone https://github.com/Aloxaf/fzf-tab ~/.config/zsh/fzf-tab/ 激活插件，将 fzf-tab 插件附加到 .zshrc 末尾：\necho \u0026#39;source ~/.config/zsh/fzf-tab/fzf-tab.plugin.zsh\u0026#39; \u0026gt;\u0026gt; ~/.config/zsh/.zshrc 效果如下：\n译注：文中缺少了一个步骤，还需要安装 fzf 命令, macos 安装： brew install fzf。\n译注：这个插件交互体验不错，但整体感觉下来不如 zsh-autosuggestions 操作快捷，先不启动了，大家可以试个人情况而定。另外，在测试过程中，我发现，zsh-autosuggestions 与 fzf-tab 似乎是无法同时启用的。\n使用 zsh-autocomplete # zsh-autocomplete，受大家欢迎的自动补齐插件之一，它提供了一些高级的命令。\n说明，zsh-autocomplete 和其他插件存在一些兼容性问题。故而，我们将用 zsh-autocomplete 默认提供的 .zshrc 配置测试。\n操作前，备份当前 .zshrc。\n# If it is in home mv ~/.zshrc ~/zshrc # If it is in .config/zsh mv ~/.config/zsh/.zshrc ~/.config/zsh/zshrc 下载并安装插件：\n# Change your directory to ~/.config/zsh first, create if not exists cd ~/ git clone https://github.com/marlonrichert/zsh-autocomplete mv zsh-autocomplete/.zshrc ~/ 源码仓库中的 .zshrc，未设置插件路径。建议进入 ~/.zshrc 手动设置或使用 sed 命令更改：\nsed -i \u0026#39;s|/path/to|~/.config/zsh/zsh-autocomplete|g\u0026#39; ~/.zshrc 译注：有两个问题。\nzsh-autocomplete 源码最新版目录下无 .zshrc，要 checkout 到 22.01.21 tag 下，才能继续. sed 在 macos 下存在兼容性问题，要加上 -e 选项，才能正常工作。 译注：读了下 zsh-autocomplete 说明文档，其中，没有 oh-my-zsh 的安装说明。\n重新打开终端，输入查看效果：\n按键绑定 # Key Bindings，即按键绑定，它让 zsh 给你带来更加丝滑的体验，诸如实现将按键绑定到特定功能、启动程序、复制粘贴等。注意，部分组合键可能无效，shell 捕捉按键的能力是有限的。\n要注意的是，在 zsh 中进行按键绑定，要用 ASCII 方式表示按键。\n如下代码，将 TAB 绑定到 fzf-tab 插件，从而在 vi 插入模式，通过 Tab 调用 fzf 实现补全。\nbindkey -M viins \u0026#39;^I\u0026#39; fzf-tab-complete 译注： Ctrl+I 与 Tab 在按键效果上等通。虽然这里绑定的是 Ctrl+I，但也等同于绑定 Tab。其他类情况还有，Ctrl+M 等同于 Enter，Ctrl+H 等同于 backspace 等。更多查看 bash 快捷键。\noh-my-zsh 或其他插件提供的功能函数，都可以通过这种方式绑定到相应的快捷键。\n窗口标题 # Shell 一般会提供设置窗口标题的能力。如当使用如 Alt+Tab 切换窗口时，可通过标题快速发现目标。我们可以通过查看窗口标题来确认终端当前目录。\n注意，窗口标题的位置，或功能上在不同环境下可能有所差异，因为每个环境有自己的实现。\n有一种方法可以通过使用 .zshrc 中的函数来动态更改窗口标题。代码实现不多解释啊了，但放心在你的 .zshrc 中使用：\ncase \u0026#34;$TERM\u0026#34; in (rxvt|rxvt-*|st|st-*|*xterm*|(dt|k|E)term) local term_title () { print -n \u0026#34;\\e]0;${(j: :q)@}\\a\u0026#34; } precmd () { local DIR=\u0026#34;$(print -P \u0026#39;[%c]\u0026#39;)\u0026#34; term_title \u0026#34;$DIR\u0026#34; \u0026#34;st\u0026#34; } preexec () { local DIR=\u0026#34;$(print -P \u0026#39;[%c]%#\u0026#39;)\u0026#34; local CMD=\u0026#34;${(j:\\n:)${(f)1}}\u0026#34; #term_title \u0026#34;$DIR\u0026#34; \u0026#34;$CMD\u0026#34; use this if you want directory in command, below only prints program name term_title \u0026#34;$CMD\u0026#34; } ;; esac 这段代码会将当前工作目录名作为窗口标题，效果如下：\n译注：在 iTerm2 下，这段代码还需要一些特殊处理，才能生效。具体细节可查看这个回答 Change iTerm2 window and tab titles in zsh\nZsh 命令 # 在 zsh 下，有一些特定命令可用于提高你的工作效率。\n十六进制数转十进制：\n$ echo $((16#ff)) 255 十进制转十六进制：\n$ echo $(([##16]255)) FF 打印 ASCII 字符整数值：\n$ echo $((#\\a)) 97 字符串拆分为数组：\n$ echo \u0026#34;${(@f)$(echo hello\\nworld)}\u0026#34; hello world 自定义分隔符拆分字符串为数组：\n$ echo \u0026#34;${(s._.)$(echo hello_world_linux)}\u0026#34; hello world linux 以上用 _ 分隔这段字符串。\n请注意，您可以使用 \u0026lsquo;.\u0026rsquo; 或 \u0026lsquo;:\u0026rsquo; 作为分隔符。\n示例：\n(s.:.): 分割字符串是 : (s:.:)：分割字符串是 . 如下为译者补充示例：\necho \u0026#34;${(s.:.)$(echo hello:world.linux)}\u0026#34; hello world.linux $ echo \u0026#34;${(s:.:)$(echo hello:world.linux)}\u0026#34; hello:world linux 总结 # 从本文可知，zsh 提供了相当多的能力，初看可能略感复杂。但如果你坚持使用，它将会成为你日常工作中不可获取的工具。\n我的博文：从 0 开始：教你如何配置 zsh,译：How to Use Zsh: A Beginer\u0026rsquo;s Guide\n","date":"2023-09-17","externalUrl":null,"permalink":"/posts/2023-09-16-how-to-use-zsh-a-beginner-guide/","section":"文章","summary":"本文将介绍如何使用 zsh 来提升命令行的操作效率。\n你是否每天都在与命令行打交道？\n如果答案是 “Yes”，那你肯定想拥有一个强大可定制的 Shell。 而 zsh 就是为这个目标而生，它运行于诸如 Linux 、MacOS 等类 Unix 系统下，可替换默认的 bash。\n","title":"从 0 开始：教你如何配置 zsh","type":"posts"},{"content":"介绍一个最快速的方式使 iTerm2 启动默认进入 Tmux 模式。默认情况下，每次启动 iTerm2，还需要一步输入 tmux attach 进入到 tmux 模式下。\n我用 Tmux 是为了管理不同项目的工作区，常见的 IDE 一般够提供了打开提供给用户一个选择项目的界面。自然而然，iTerm2 + Tmux 是否也能实现类似的能力呢？\n非常简单！\n核心脚本介绍\n首先，一段 bash 脚本:\ntmux ls \u0026amp;\u0026amp; read -p \u0026#34;Select a session\u0026lt;default\u0026gt;:\u0026#34; tmux_session \u0026amp;\u0026amp; tmux attach -t ${tmux_session:-default} || tmux new -s ${tmux_session:-default} 这段脚本的说明如下：\ntmux ls, 先输出当前可用的 session 列表，供用户输入使用； read xxx, 读取用户输入，将希望打开的会话名称存入 tmux_session 中； tmux attach，尝试打开会话，如果 tmux_session 为空，打开 default 会话； tmux new，如果开启失败，尝试创建一个新的会话； 说明：由于 read 命令使用了-p，必须要使用 bash 运行这段脚本。\n配置 iTerm2 启动加载\n选择 Preference -\u0026gt; Profile -\u0026gt; General -\u0026gt; Command -\u0026gt; Select \u0026ldquo;Login Sell\u0026rdquo;，并将代码放到 \u0026ldquo;Send text at start\u0026rdquo; 的输入框即可。\n效果如下：\nhello: 1 windows (created Tue Sep 12 17:13:54 2023) (group hello) (attached) default: 1 windows (created Wed Sep 13 19:54:36 2023) (group default) Select a session\u0026lt;default\u0026gt;: 输入 \u0026ldquo;hello\u0026rdquo;，即可进入 \u0026ldquo;hello\u0026rdquo; 会话。简洁高效，有点想 IDE 启动的之后，让我们选择项目的感觉。\n如果希望会话列表足够简洁，只有会话名称，可用 tmux ls 的输出格式化默认，将 tmux ls 进化为 tmux -F '\\#{session_name}'；\nhello default 尝试升级改进\n能不能只输入索引编号，实现快速选择呢？这种简单的 shell script 不容易实现。写了一段 Python 脚本，如下所示：\n#!/usr/bin/env python import os output = os.popen(\u0026#34;tmux ls -F \\\\#{session_name}\u0026#34;).read() sessions = output.strip().split(\u0026#34;\\n\u0026#34;) print(\u0026#34;Sessions:\u0026#34;) for index in range(len(sessions)): print(f\u0026#34;{index} - {sessions[index]}\u0026#34;) input_value = input(\u0026#34;Please select a session \u0026lt;Index or Name\u0026gt;(default):\u0026#34;) if input_value.isdigit(): sess_name = sessions[int(input_value)] elif not input_value: sess_name = \u0026#34;default\u0026#34; else: sess_name = input_value if sess_name not in sessions: answer = input(f\u0026#34;New a session `{sess_name}`?(Y/N)\u0026#34;) answer == \u0026#34;Y\u0026#34; and os.system(f\u0026#34;tmux new -s {sess_name}\u0026#34;) # pyright: ignore else: os.system(f\u0026#34;tmux attach -t {sess_name}\u0026#34;) 按上面的方式将脚本配置到 iTerm2 中，启动效果:\n❯ ~/tmux_selector Sessions: 0 - hello 1 - default Please select a session \u0026lt;Index or Name\u0026gt;(default): 这里只是一个小 demo，tmux ls 的输入还有其他格式化变量，有兴趣可以继续扩展。代码片段 github 地址。\n总体上的体验还不错，但如果使用自带的功能，岂不是更好？继续探索吧。\n使用 tmux 内置菜单树\n进入 tmux 模式后，默认可以用快捷键 prefix-key + s 启动 tmux 选择菜单：\nhello: 1 windows (created Tue Sep 12 17:13:54 2023) (group hello) (attached) default: 1 windows (created Wed Sep 13 19:54:36 2023) (group default) 而且可以使用 jk 移动选择菜单。这个有没有办法在 iTerm2 启动时直接启动呢？想法很好，实现起来也不难。\n首先，通过 tmux list-keys 检查 prefix-key + s 绑定的命令 tmux 命令。\n$ tmux list-keys ... bind-key -T prefix s choose-tree -Zs ... 通过 choose-tree -Zs 即可拉起菜单。在 tmux 模式下，使用 tmux choose-tree -Zs 测试一下效果，选择窗口成功拉起。\n那么，接下来的问题是，如何配置到 iTerm2 启动命令呢？\n因为这个命令必须在 tmux 模式下运行。如何做？通过 tmux attach\\; \u0026lt;other commands running on tmux mode\u0026gt; 即可实现。\n完整命令如下：\n$ tmux attach\\; choose-tree -zS 试着在非 tmux 模式下测试下这段代码，看看是否能成功拉起菜单。确认没有问题后，将上面的语句配置到你的 iTerm2 启动时执行即可。\n最后，为了防止首次进入没有 session，进一步优化，创建默认的 default 会话窗口。\n完整 shell 代码如下：\n$ tmux attach\\; choose-tree -Zs || read -p \u0026#34;Create a default session?(Y/N)\u0026#34; anwser \u0026amp;\u0026amp; [[ \u0026#34;${anwser}\u0026#34; == \u0026#34;Y\u0026#34; ]] \u0026amp;\u0026amp; tmux new -t default 但这里还有个缺点，默认 q 只能退出菜单，终端还是在 tmux 模式会话中，还要多一步 detach 才能退出，这算是和我们一般认为的步骤不太一样的地址吧。\n","date":"2023-09-15","externalUrl":null,"permalink":"/posts/2023-09-15-autostart-tmux-in-iterm2/","section":"文章","summary":"介绍一个最快速的方式使 iTerm2 启动默认进入 Tmux 模式。默认情况下，每次启动 iTerm2，还需要一步输入 tmux attach 进入到 tmux 模式下。\n我用 Tmux 是为了管理不同项目的工作区，常见的 IDE 一般够提供了打开提供给用户一个选择项目的界面。自然而然，iTerm2 + Tmux 是否也能实现类似的能力呢？\n","title":"iTerm2 启动时进入 Tmux 模式","type":"posts"},{"content":"","date":"2023-09-15","externalUrl":null,"permalink":"/tags/tmux/","section":"Tags","summary":"","title":"Tmux","type":"tags"},{"content":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。系统通过这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n本文是 \u0026ldquo;构建属于自己的 Web 框架\u0026rdquo; 系列文章中的第二篇，将介绍中间件的最佳实践。\n第 1 部分：简介，Build Your Own Web Framework In Go 第 2 部分：Go 中间件：最佳实践和示例，Part 2: Middlewares in Go: Best practices and examples 第 3 部分：中间件数据共享，Part 3: Share Values Between Middlewares 第 4 部分：第三方路由，Part 4: Guide to 3rd Party Routers in Golang 第 5 部分：使用 MongoDB 实现 JSON-API，How to implement JSON-API standard in MongoDB and Go 附加福利：上传文件到 s3，Bonus: File Upload REST API with Go and Amazon S3 在编写 Go Web 应用时，代码重复是大多数开发者将会遇到的第一个问题。\n为什么呢？\n原因在于，在处理 request 时，诸如记录请求、将应用程序错误转换为 HTTP 500 错误、验证用户等一些操作，这是每个处理程序都要执行的动作。\n基础入门 # 首先，使用 net/http 包创建一个简单版本的 HTTP Server 应用。\n代码如下：\nimport ( \u0026#34;net/http\u0026#34; \u0026#34;fmt\u0026#34; ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome!\u0026#34;) } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, handler) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 阅读以上代码，我们看出 http.HandleFunc 通过接受参数 location pattern 和 handler，实现特定路径与处理程序的匹配映射。handler 有 2 个参数，分别是 response writer 和 request，分别用于请求响应写入和请求信息读取。\n除了通过 http.HandleFunc 指定处理函数的方式，实现 http.Handler 接口也可以帮助我们达成同样目的。\n接口定义如下：\ntype Handler interface { ServeHTTP(ResponseWriter, *Request) } 示例代码如下：\ntype handler struct {} func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome!\u0026#34;) } func main() { http.Handle(\u0026#34;/\u0026#34;, handler) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 一旦实现了 http.Handler 的 ServeHTTP(http.ResponseWriter, *http.Request) 方法， 就能被 Go muxer (http.Handle(pattern, handler) function) 使用。\n添加日志 # 现在，我们希望通过增加一个简单的日志，记录处理每个请求所花费的时间。\n代码如下：\nfunc indexHandler(w http.ResponseWriter, r *http.Request) { t1 := time.Now() fmt.Fprintf(w, \u0026#34;Welcome!\u0026#34;) t2 := time.Now() log.Printf(\u0026#34;[%s] %q %v\\n\u0026#34;, r.Method, r.URL.String(), t2.Sub(t1)) } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, indexHandler) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 输出内容如下：\n[GET] / 1.43ms [GET] /about 1.98ms 继续，我们增加第二个 handler。毕竟，只有一个路由的应用程序并不多。\nfunc aboutHandler(w http.ResponseWriter, r *http.Request) { t1 := time.Now() fmt.Fprintf(w, \u0026#34;You are on the about page.\u0026#34;) t2 := time.Now() log.Printf(\u0026#34;[%s] %q %v\\n\u0026#34;, r.Method, r.URL.String(), t2.Sub(t1)) } func indexHandler(w http.ResponseWriter, r *http.Request) { t1 := time.Now() fmt.Fprintf(w, \u0026#34;Welcome!\u0026#34;) t2 := time.Now() log.Printf(\u0026#34;[%s] %q %v\\n\u0026#34;, r.Method, r.URL.String(), t2.Sub(t1)) } func main() { http.HandleFunc(\u0026#34;/about\u0026#34;, aboutHandler) http.HandleFunc(\u0026#34;/\u0026#34;, indexHandler) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 代码重复！ 该如何解决呢？\n我们可以创建一个带有闭包的函数。但是，当我们有多个这样的函数时，它就会变得像 Javascript 中的回调一样糟糕，我们并不想如此。\n链接处理 # 我们希望有一种类似 Rack、Ring、Connect.js 等的中间件系统的解决方案，链接多个处理程序。标准库中已经有这种实现的示例：\nhttp.StripPrefix(prefix, handler)； http.TimeoutHandler(handler, duration, message)。 它们将 handler 作为参数传递，返回一个新的 handler。如下所示：\nloggingHandler(recoverHandler(indexHandler)) 中间件类似于 func (http.Handler) http.Handler，我们传递一个 handler 并返回一个 handler。我们就可以用 http.Handle(pattern, handler) 得到目标的处理程序。\n实现代码如下：\nfunc main() { http.Handle(\u0026#34;/\u0026#34;, loggingHandler(recoverHandler(indexHandler))) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 但如此还是很麻烦，一遍又一遍地重复堆栈。 有没有什么更优雅的方式实现呢？\n通用包 alice # alice 是一个非常短小精悍的包，它优雅地实现了 handler 的链接调用。通过它，我们可以创建一个通用的 handler 列表，便于我们重复使用。\nfunc main() { commonHandlers := alice.New(loggingHandler, recoverHandler) http.Handle(\u0026#34;/about\u0026#34;, commonHandlers.ThenFunc(aboutHandler)) http.Handle(\u0026#34;/\u0026#34;, alice.New(commonHandlers, bodyParserHandler).ThenFunc(indexHandler)) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 问题解决！\n我们已经有了一个使用标准接口的中间件系统，alice 有 50 行代码，一个非常小的依赖。如果想了解 alice 的实现细节，可自行阅读 alice 源码。\n多个参数的 Handler # alice 这样的中间件系统中，我们仍然不能使用类似 http.StripPrefix(prefix, handler) 具有多个参数的 handler。因为，它不是 func (http.Handler) http.Handler 类型函数。\n怎么办？\n我们可以通过定义新的 handler 实现兼容效果，保证满足 func (http.Handler) http.Handler。\nfunc myStripPrefix(h http.Handler) http.Handler { return http.StripPrefix(\u0026#34;/old\u0026#34;, h) } 现在，新的 handler 我们在 alice 中间件系统可以开始使用了。\n再谈 logging middleware # 通过 alice，我们有了更加优雅的方式实现代码重复的删除。我们无需重新定义的一个新的 http.Handler 接口，标准接口即可满足要求，这意味学习成本非常低，依赖更少。\n实现代码如下：\nfunc loggingHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { t1 := time.Now() next.ServeHTTP(w, r) t2 := time.Now() log.Printf(\u0026#34;[%s] %q %v\\n\u0026#34;, r.Method, r.URL.String(), t2.Sub(t1)) } return http.HandlerFunc(fn) } 最后，我们使用 alice 将 loggingHandler 与其他 handler 链接起来。\nfunc aboutHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;You are on the about page.\u0026#34;) } func indexHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome!\u0026#34;) } func main() { commonHandlers := alice.New(loggingHandler) http.Handle(\u0026#34;/about\u0026#34;, commonHandlers.ThenFunc(aboutHandler)) http.Handle(\u0026#34;/\u0026#34;, commonHandlers.ThenFunc(indexHandler)) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 大功告成！\n新中间件：panic recovery # 另一个真正必要的功能：panic recovery。\n当生产环境出现 panic，应用程序会被关闭。即使，我们有一个监控负责应用程序检测重启，也会不可避免的停机一小段时间。我们必须捕捉和记录 panic，并保持应用程序运行。\n使用 Go 和中间件系统，这会变得非常容易。 只需要我们创建一个 defer 函数恢复 panic，响应 HTTP 500 错误并记录 panic 即可。\nfunc recoverHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf(\u0026#34;panic: %+v\u0026#34;, err) http.Error(w, http.StatusText(500), 500) } }() next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } 现在，将它追加到我们的中间件 stack 中。\nfunc main() { commonHandlers := alice.New(loggingHandler, recoverHandler) http.Handle(\u0026#34;/about\u0026#34;, commonHandlers.ThenFunc(aboutHandler)) http.Handle(\u0026#34;/\u0026#34;, commonHandlers.ThenFunc(indexHandler)) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 总结 # 我们已经了解，func (http.Handler) http.Handler 是一种非常简单的中间件定义方法，它提供了一切所需，http.Handler 是一个标准接口。\n而通过链接方式实现中间件系统，已经是非常流行的方案，比如 Gorilla 和标准库本身。 我认为这是最惯用的方式。\n我们已经编写了两个中间件：logging 和 panice recovery。\n几乎每个框架中都在重写它们，尽管它们的功能几乎一样。大多数框架都有自己特定的 handler 定义，很难与其他中间件协同使用。接下来的一部分，我们将会了解到，在中间件之间共享值时，我们可能要更改一些内容。但它其实没有那么大的变化，我们没有理由重写已有的中间件。\n博文地址：从头构建 Go Web 框架（二），译：Part2: Middlewares in Go: Best practices and examples。\n","date":"2021-10-28","externalUrl":null,"permalink":"/posts/2021-10-28-build-your-own-webframework-in-golang-part-2/","section":"文章","summary":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。系统通过这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n","title":"从头构建 Go Web 框架（二）：中间件","type":"posts"},{"content":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。系统通过这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n本文是 \u0026ldquo;构建属于自己的 Web 框架\u0026rdquo; 系列文章中的第二篇，将介绍中间件的最佳实践。\n第 1 部分：简介，Build Your Own Web Framework In Go 第 2 部分：Go 中间件：最佳实践和示例，Part 2: Middlewares in Go: Best practices and examples 第 3 部分：中间件数据共享，Part 3: Share Values Between Middlewares 第 4 部分：第三方路由，Part 4: Guide to 3rd Party Routers in Golang 第 5 部分：使用 MongoDB 实现 JSON-API，How to implement JSON-API standard in MongoDB and Go 附加福利：上传文件到 s3，Bonus: File Upload REST API with Go and Amazon S3 Martini 发布之后，迅速成为了最受大家欢迎的 Go 语言 Web 框架，且现在依旧是如此。但必须指出的是，它不符合常规习惯，非常慢，概念也有不足。它教了我们一堆错误的做法。但因为它上手容易，许多开发人员仍在使用。\n似乎是存在即合理！\n故而，我决定写一系列文章，基于现有库从头编写组件来构建自己的 Web 框架。在开始前，我搜罗和阅读了市面上绝大部分关于如何编写 Go Web 应用的资料。我希望，通过系列文章能教授 Go Web 开发人员一些最佳实践，同时能提醒老 Go 开发人员什么才是 Web 开发的最佳实践。\n概要 # 本系列不仅能让你了解 Go 中 Web 开发的最佳实践，还会让你了解其他常见问题的解决方案，以及如何正确运用其他框架。\n框架该具备什么能力 # 市面主要有两种类型的 Web 框架。\n一种是内置所有功能，类似 Rails 的框架，能帮你快速构建与引导项目。Go 中类似框架有：Beego 和 Revel。\n另一种，类似于 Sinatra 的框架，提供路由和一些内置功能，但不会提供 ORM 等功能。 多数 Go 框架都采用了这种风格，如 Martini、Goji、gocraft/web 等。\n框架与库 # 最近，发生了一场争论，主题是，究竟是该使用框架还是使用库呢？\n首先，我不反对框架，但 Go 有很棒的小包，能帮助我们非常容易地制作出自己的框架。特别是长期项目，自己动手是更明智的选择。如果你想学习围棋，最好了解一切是如何运作的。\n争论详情查看以下文章：\nThe Case for Go Web Frameworks Go, frameworks, and Ludditry Frameworks: Necessary for large-scale software Framework vs Library\n框架功能 # 我们将专注于后一种类型，因为涵盖 Beego 或 Revel 中的每个功能将意味着针对该主题编写一整本书！\n框架主要包含 3 个部分，分别是：\n一是 router，负责接收 request 并路由到指定 handler； 二是 middleware，handler 前后新增的复用性代码； 三是 handler 处理程序，负责处理请求并写入响应。 常见的中间件 Middleware：\nerror/panic logging security sessions cookies body parsing Go Web 框架现状 # 现在，许多 Go Web 框架的问题在于，它们的组件仅适用于自身，是不可替换的。\n假设现在有这样一些小包，只有路由器，或只有中间件系统，或者只有中间件，且它们彼此兼容，我们就可以将它们组合使用。 但现实是，这类项目数量极少，除了 Gorilla 之外，基本没什么知名度。当然，即使是 Gorilla，在 Github 上的追随者与其他大多数其他框架，依然少很多。\nGo 的 Middleware 系统并没有统一的约定惯例。与 Ruby (Rack)、Node.js (Connect.js)、Clojure (Ring) 和其他语言不同。 每个框架，如 Negroni、Goji、Gin、gocraft/web 等，都在重新定义自己的中间件工作方式，重用开源中间件变得不可能。\n什么才是开发人员的最佳实践？请阅读下一篇，从头构建 Go Web 框架。\n博文地址：从头构建 Go Web 框架，译：Build Your Own Framework in Golang。\n","date":"2021-10-23","externalUrl":null,"permalink":"/posts/2021-10-23-build-your-own-webframework-in-golang/","section":"文章","summary":"本系列文章写于 2014 年，相较于 golang 极短的发展历程，这已经是古董级别的一篇文章了，但 web 框架思想概念依然有效。系统通过这个系列文章，能让大家都现有 Go Web 框架有更深的认识。\n","title":"从头构建 Go Web 框架（一）：介绍","type":"posts"},{"content":"写于 2021 年 7 月份，当时公司在推自动化测试方案，集成流水线的时候，研究了这个方案。\n本文介绍的是如何基于 bilibili 的开源方案 powermock 搭建一套通用的适用于自己公司的 MockServer。\n背景 # 我所在公司正处在一个高速发展的阶段，各产品线齐头并进。而我所在的部门主要负责核心能力建设与增长类业务，属于所有产品线的最下游。\n业务部门希望在新产品部署产线后，我们能快速的配合。这就导致了一个非常尴尬的局面，产品确定上线时间，处在产品上线的最后阶段的我们，如果有任何异常，都可能导致上线时间被压缩。\n如何防止这类情况？简单来说，就是如何防止因依赖导致项目开发的不可预期。\n一个常见拉新活动的业务图为例，如下：\n用户场景是，假设一名用户通过我的邀请码完成平台注册，并且完成首次购物，他才能算作我的成功邀请用户，我才能得到我想要的奖励。\n一个简单的拉新活动，因为服务拆分，需要同时依赖于两个服务才可以能完成这一个活动的开发。我们的核心诉求是测试我们自己开发的邀请活动。而用户和订单服务，一方面不在测试范围之内，另一方面，这些服务是其他团队开发，在测试环境的稳定性没人保证，会成为开发排期的瓶颈，而且如果是并行开发，这些服务可能还没有完成。\n如果有一个服务，能够实现依赖服务协议，方便我们尽可能的穷举依赖服务的各种场景，让我们不需要时时刻刻的依赖上游服务，是不是就能解决这个问题？\n选型 # 基于这些困惑和一段时间的摸索，团队成员提出了一套新的解决思路，基于 Mock 方式解决问题。确定了这个思想，接下来就是如何实施了。\n通常服务间的依赖可分两类，一类是由被依赖方主动触发的消息，二类是由依赖方主动发起的调用。消息类依赖主要容易 mock，而服务间的调用 mock 相对复杂。\n当前的微服务架构下，gRPC 是主流的服务间调用的协议，Mock Server 必然需要支持，经过一番寻找，在市面上发现了最近 bilibili 的开源实现方案 powermock。\n这个工具的开源看时间在 2021 年 5 份刚刚开源，powermock 同时支持 HTTP 与 gRPC 协议接口的 Mock，提供灵活的插件功能。面向对象包括前后端（HTTP、gRPC）、测试等对 Mock 有需求的所有人员。\n当前这个项目的 star 为 5，顺手 star 加到了 6。虽然 star 有点少，但鉴于其特性的确是我想要的功能，肯定是要尝试一下的。powermock 最吸引我的地方在于，代码简单，易于阅读，二次开发方便。而且，对于 gRPC 的支持是一个亮点。市面上的 Mock Server 主要都是面向 HTTP 的接口，面向前端。\n架构 # 为便于针对 powermock 二次开发，通过阅读源码，我整理出 powermock 的主体架构，如下所示：\n从上图可以看出， powermock 本身是一个 server，提供了 HTTP 和 gRPC 两种接入方式，即通过它可以 mock HTTP 和 gRPC 两种服务。\nMock 服务的对象是作为调用方的我们，我们希望 Mock Server 能模拟依赖服务的行为，如此才能帮助我们解决环境依赖的问题， ApiManager 提供的用于指定 Mock Server 行为规则的接口。\n插件是 powermock 扩展功能不可少的能力，通过阅读源码，主要有三类插件，分别是：\nMatchPlugin，用于解析请求到响应的匹配规则，如指定请求 id 为 1 的情况下该返回什么内容。当前支持插件有 simple （YAML 指定匹配规则） 和 script （Javascript 指定匹配规则）插件；\nStoragePlugin，用于保存 Mock 规则，支持的插件有 redis 和 rediscluster 三类插件。默认规则保存在内存里，重启即丢失。redis 可永久保存规则，但没有将数据结构化存储，大规划构造测试数据不是很方便；\nMockPlugin，用于生成 Mock 数据，包含三个插件，分别是有 HTTP 和 gRPC，负责将 response 按 gRPC 或者 HTTP 格式返回；\n整体的架构大概如此，理解了它们，对于接下来的无论是安装配置，还是具体的使用都会有大有裨益。\n安装 # powermock 提供了 powermock 和 powermock-v8（支持 javascript 脚本解析规则） 两个命令。\n安装方式如下：\n$ go get github.com/bilibili-base/powermock/cmd/powermock $ go get github.com/bilibili-base/powermock/cmd/powermock-v8 // 支持 javascript 的 server 配置 # powermock 的配置分为两类，一个 mockserver 本身的配置，另外则是对于 Mock 规则的配置。\n这小节介绍的内容主要是 Mock Server 自身的配置。如下是一个示例配置：\nlog: pretty: true level: debug grpcmockserver: enable: true address: 0.0.0.0:30002 protomanager: protoimportpaths: [ ] protodir: ./apis httpmockserver: enable: true address: 0.0.0.0:30003 apimanager: grpcaddress: 0.0.0.0:30000 httpaddress: 0.0.0.0:30001 pluginregistry: { } plugin: simple: { } grpc: { } http: { } script: { } redis: enable: false addr: 127.0.0.1:6379 password: \u0026#34;\u0026#34; db: 0 prefix: /powermock/ 如果已经了解前面的架构，这里的配置就比较容易理解了。\n包括：\nmock server 配置，主要是 gRPC 和 HTTP 两类服务的地址与端口配置； apimanager 配置，指定 grpc 和 http 的 mock API 规则管理服务的接入地址和端口； plugin 插件配置，插件的管理配置，这里吐槽一下， powermock 将所有类别的插件在一个 plugin 配置项下，类别区分不明晰，如果增加二级目录，如 mock、match、storage，配置会更加易懂； 使用教程 # 接下来，我将通过几个真实的案例带大家一起看下 powermock 的使用方法，如何实现 proto 和 http 服务的 Mock。\n首先，创建目录 examples/，将所有的 proto 文件定义整理到这个文件下。\n快速开始 # 先来介绍一个官方的快速接入的案例，介绍如何通过内存和 redis 存储 Mock 规则。\n接口定义 # 首选，创建 greeter/apis/greeter.proto 文件，内容如下：\nsyntax = \u0026#34;proto3\u0026#34;; package examples.hello.api; option go_package = \u0026#34;github.com/poloxue/mock/examples/greeter\u0026#34;; service Greeter { rpc Hello(HelloRequest) returns (HelloResponse); } message HelloRequest { string message = 2; } message HelloResponse { string message = 2; } 在 greeter 目录下执行使用 protoc 命令编译 proto 文件，如下：\n$ protoc -I. --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. ./apis/*.proto 编译生成文件包括 greeter/apis/greeter.pb.go 和 greeter/apis/greeter_grpc.pb.go。\n配置启动 # 首先是一个基本的内存版本的 mockserver，创建 greeter/config.yaml，配置如下：\nlog: pretty: true level: debug grpcmockserver: enable: true address: 0.0.0.0:30002 protomanager: protoimportpaths: [] protodir: ./apis httpmockserver: enable: true address: 0.0.0.0:30003 apimanager: grpcaddress: 0.0.0.0:30000 httpaddress: 0.0.0.0:30001 pluginregistry: { } plugin: simple: enable: true 配置的具体含义参考前面的介绍，主要的是启用的 plugin 是 simple，最简单的内存版本。执行如下命令启动服务：\n$ powermock serve --config.file config.yaml 9:18PM INF * start to create pluginRegistry component=main file=setup.go:40 9:18PM INF * start to create apiManager component=main file=setup.go:46 9:18PM INF * start to create httpMockServer component=main file=setup.go:63 ... 9:18PM INF starting service component=main.gRPCMockServer.gRPC file=service.go:29 9:18PM INF starting service component=main.apiManager.http file=service.go:29 9:18PM INF starting service component=main.httpMockServer file=service.go:29 输出日志，成功启动服务。\nMock 规则 # 成功启动服务之后，如何设定配置 Mock 数据的生产规则呢？即什么输入对应输出的规则。\n先来一个简单的配置目标，无论输入什么都返回固定的 {\u0026ldquo;message\u0026rdquo;: \u0026ldquo;hello world!\u0026quot;}。\n创建文件 greeter/apis.yaml，配置如下：\nuniqueKey: \u0026#34;hello_example_gRPC\u0026#34; path: \u0026#34;/examples.greeter.api.Greeter/Hello\u0026#34; method: \u0026#34;POST\u0026#34; cases: - response: simple: header: x-unit-id: \u0026#34;3\u0026#34; x-unit-region: \u0026#34;sh\u0026#34; trailer: x-api-version: \u0026#34;1.3.2\u0026#34; body: | {\u0026#34;message\u0026#34;: \u0026#34;hello world!\u0026#34;} 上述不仅设置了响应的 body，还设置了 x-unit-id 等 header 和 grpc trailer 信息。\n如何将规则加载到 mock server 中，命令如下：\n$ powermock load --address=127.0.0.1:30000 apis.yaml 加载完成后，我们可以直接通过 grpcurl 命令验证下 Mock 的结果是否是我们想要的。\n$ protoc --proto_path=. --descriptor_set_out=greeter.protoset --include_imports apis/*.proto $ grpcurl -plaintext -protoset greeter.protoset 127.0.0.1:30002 examples.greeter.api.Greeter/Hello { \u0026#34;message\u0026#34;: \u0026#34;hello world!\u0026#34; } 通过 powermock 的 load 将 apis 配置加载到 mock server 的内存中，但这种方式，如果一旦我们重启了 mock server，先前定义的配置就会失效。\n有没有持久化的方案呢？有，就是接下来要介绍的 redis 方案。\nredis 插件 # 通过将 config.yaml 的 simple 配置更新为 redis，如下：\nredis: enable: true addr: 127.0.0.1:6379 password: \u0026#34;\u0026#34; db: 0 prefix: /mockserver/ 重启 Mock 服务之后，再次 load apis 配置信息。\n127.0.0.1:6379\u0026gt; keys * 1) \u0026#34;/mockserver/hello_example_gRPC\u0026#34; 2) \u0026#34;/mockserver/__REVISION__\u0026#34; 通过 redis 持久化存储的 Mock 规则的好处不言而喻。\n不断补充依赖服务在正式场景下的规则，逐步完成一个与真实场景几近相同的 Mock Server，无论是开发人员效率，还是自动化测试的场景覆盖都有极大的提升。\nHTTP Mock # 前面介绍的都是 gRPC 接口的 Mock，作为后端开发的我肯定更加关心的这个。如果是前端更关心的肯定是 HTTP 接口的 Mock。apis 的 Mock 也非常简单。\n配置目标，无论什么情况下，都返回消息 \u0026ldquo;hello world\u0026rdquo;，在 apis.yaml 中新增如下配置：\nuniqueKey: \u0026#34;hello_example_http\u0026#34; path: \u0026#34;/hello\u0026#34; method: \u0026#34;GET\u0026#34; cases: - response: simple: header: x-unit-id: \u0026#34;3\u0026#34; x-unit-region: \u0026#34;sh\u0026#34; trailer: x-api-vesion: \u0026#34;1.3.2\u0026#34; body: hello world! 通过 curl 命令测试下 mock 结果，如下：\n$ curl -v http://localhost:30003/hello * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 30003 (#0) \u0026gt; GET /hello HTTP/1.1 \u0026gt; Host: localhost:30003 \u0026gt; User-Agent: curl/7.64.1 \u0026gt; Accept: */* \u0026gt; \u0026lt; HTTP/1.1 200 OK \u0026lt; Content-Type: application/json \u0026lt; X-Unit-Id: 3 \u0026lt; X-Unit-Region: sh \u0026lt; Date: Sun, 11 Jul 2021 08:06:57 GMT \u0026lt; Content-Length: 12 \u0026lt; * Connection #0 to host localhost left intact hello world!* Closing connection 0 至此，一个 HTTP 接口的 Mock 已经完成。\n高级配置 # 前面的内容主要介绍如何快速上手这个新的 Mock Server。但在真实场景下，伪造数据场景复杂，只返回固定的信息肯定不满足我们的要求。\n如何处理呢？这就需要 apis.yaml 支持更多的高级 Mock 规则。\n前置准备 # 还是以一个真实的场景为例吧，一个简单的用户服务的 proto 接口定义。\nsyntax = \u0026#34;proto3\u0026#34;; package examples.user.api; option go_package = \u0026#34;github.com/poloxue/mock/examples/user/apis\u0026#34;; service User { rpc Register(GetUserRequest) returns(GetUserResponse); rpc SetUserDetail(SetUserDetailRequest) returns(SetUserDetailRequest); rpc GetUser(GetUserRequest) returns(GetUserResponse); } message RegisterRequest { string mobile = 1; string password = 2; } message RegisterResponse { int32 status = 1; } message SetUserDetailRequest { int64 id = 1; string mobile = 2; string username = 3; string email = 4; } message GetUserRequest { int64 id = 1; string mobile = 2; } message GetUserResponse { int64 id = 1; string mobile = 2; string username = 3; string email = 4; } 共三个接口，分别是用户注册、设置详情以及获取用户信息。我们按 快速开始 中的步骤把 proto 的 go 文件编译，配置等准备完成。\n场景一 特定 ID 返回特定用户信息 # 如果 id 为 1000 的情况，返回用户信息如下：\n{ \u0026#34;id\u0026#34;: 1000, \u0026#34;mobile\u0026#34;: \u0026#34;15300000001\u0026#34;, \u0026#34;username\u0026#34;: \u0026#34;poloxue\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;[poloxue123@gmail.com](mailto:poloxue123@gmail.com)\u0026#34; } 配置 Mock 规则如下：\nuniqueKey: \u0026#34;get_user_gRPC\u0026#34; path: \u0026#34;/examples.user.api.User/GetUser\u0026#34; method: \u0026#34;POST\u0026#34; cases: - condition: simple: items: - operandX: \u0026#34;$request.body.id\u0026#34; operator: \u0026#34;==\u0026#34; operandY: \u0026#34;1000\u0026#34; response: simple: body: | {\u0026#34;id\u0026#34;: 1000, \u0026#34;mobile\u0026#34;: \u0026#34;15300000001\u0026#34;, \u0026#34;username\u0026#34;: \u0026#34;poloxue\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;poloxue123@gmail.com\u0026#34;} 将配置加载到 Mock Server 中，通过 grpcurl 请求数据，将得到如下结果：\n$ grpcurl -d \u0026#39;{\u0026#34;id\u0026#34;: 1000}\u0026#39; -plaintext -protoset user.protoset 127.0.0.1:30002 examples.user.api.User/GetUser { \u0026#34;id\u0026#34;: \u0026#34;1000\u0026#34;, \u0026#34;mobile\u0026#34;: \u0026#34;15300000001\u0026#34;, \u0026#34;username\u0026#34;: \u0026#34;poloxue\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;poloxue123@gmail.com\u0026#34; } 上述通过 condition 的判断，如果 $request.body.id 是 1000，则返回特定的用户数据。\n场景二 通过脚本返回用户数据 # 上述的场景按用户 ID 返回用户，如果需要返回多种场景用户，要写大量的映射规则。如果制定通用的生成规则呢？\n指定一个目标，如果用户 ID 在 500 以内，返回规则为 email 为空，mobile 规则为 1530000+4 位用户 ID，用户名为 poloxue+用户ID。\n新增的 condition 配置规则如下：\n- condition: simple: items: - operandX: \u0026#34;$request.body.id\u0026#34; operator: \u0026#34;\u0026lt;\u0026#34; operandY: \u0026#34;500\u0026#34; response: script: lang: \u0026#34;javascript\u0026#34; content: | (function () { function pad(num, size) { num = num.toString(); while (num.length \u0026lt; size) num = \u0026#34;0\u0026#34; + num; return num; } return { code: 0, body: { id: request.body.id, username: \u0026#39;username\u0026#39; + request.body.id, mobile: \u0026#39;1530000\u0026#39; + pad(request.body.id, 4), }, } })() 使用 powermock load 命令加载新的配置到 server 中。至此，如果我们使用 grpcurl 访问这个服务，得到的信息如下：\n$ grpcurl -d \u0026#39;{\u0026#34;id\u0026#34;: 100}\u0026#39; -plaintext -protoset user.protoset 127.0.0.1:30002 examples.user.api.User/GetUser ERROR: Code: Internal Message: plugin(grpc): failed to unmarshal: EOF 因为，如果希望通过脚本生成 mock 数据，要将 powermock 替换为 powermock-v8 命令才可执行javascript 脚本。\n$ grpcurl -d \u0026#39;{\u0026#34;id\u0026#34;: 1}\u0026#39; -plaintext -protoset user.protoset 127.0.0.1:30002 examples.user.api.User/GetUser { \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;mobile\u0026#34;: \u0026#34;15300000001\u0026#34;, \u0026#34;username\u0026#34;: \u0026#34;username1\u0026#34; } $ grpcurl -d \u0026#39;{\u0026#34;id\u0026#34;: 100}\u0026#39; -plaintext -protoset user.protoset 127.0.0.1:30002 examples.user.api.User/GetUser { \u0026#34;id\u0026#34;: \u0026#34;100\u0026#34;, \u0026#34;mobile\u0026#34;: \u0026#34;15300000100\u0026#34;, \u0026#34;username\u0026#34;: \u0026#34;username100\u0026#34; } 一个批量 Mock 用户数据的规则就写好了。\n总结 # 整体而言， powermock 易于使用，但刚刚开源，还有些待完善的地方。但鉴于代码易读，容易扩展，与我们而言，当前的确是一个不错的选择。\n参考资料 # Mock Server 实践 阿里Mock平台使用方法揭秘！ powermock 代码仓库 博客地址：powermock: 一个支持 gRPC 的 Mock Server\n","date":"2021-07-17","externalUrl":null,"permalink":"/posts/2021-07-17-powermock-autotest-your-code/","section":"文章","summary":"写于 2021 年 7 月份，当时公司在推自动化测试方案，集成流水线的时候，研究了这个方案。\n本文介绍的是如何基于 bilibili 的开源方案 powermock 搭建一套通用的适用于自己公司的 MockServer。\n","title":"powermock: 一个支持 gRPC 的 Mock Server","type":"posts"},{"content":"早前写过一篇文章，Go HTTP 请求 QuickStart。当时，主要参考 Python 的 requests 大纲介绍 Go 的 net/http 如何发起 HTTP 请求。\n最近，尝试录成它的视频，访问地址。发现当时写得挺详细的，发现当时虽然写得比较详细，但也只是介绍用法，可能不知其所以然。比如文件上传那部分，如果不了解 http 文件上传协议 RFC 1867，就很难搞懂为什么代码这么写。\n今天，就以这个话题为基础，介绍下 Go 如何实现文件上传。\n相关代码请访问 httpdemo/post。本文视频地址：Go 上传文件\n简介 # 简单来说，HTTP 上传文件可以分三个步骤，分别是组织请求体，设置 Content-Type 和发送 Post 请求。POST 请求就不用介绍了，主要关注请求体和请求体内容类型。\n请求体，即 request body，常用于 POST 请求上。请求体并非 POST 特有，GET 也支持，只不过约定俗成的规定，服务端一般会忽略 GET 的请求体。\nContent-Type 是什么？\n因为，请求体的格式并不固定，可能性很多，为了明确请求体内容类型，HTTP 定义了一个请求头 Content-Type。\n常见的 Content-Type 选项有 application/x-www-form-urlencoded（默认的表单提交）、application/json（json）、text/xml（xml 格式）、text/plain（纯文本）、application/octet-stream（二进制流）等。\n提交表单 # 文件上传可以理解为是提交表单的特例，先通过表单提交这个简单的例子介绍下整个流程。\n如下是表单提交的 HTTP 请求文本。\nPOST http://httpbin.org/post HTTP/1.1 Content-Type: application/x-www-form-urlencoded username=poloxue\u0026amp;password=123456 Content-Type 是 application/x-www-form-urlencoded，数据通过 urlencoded 方式组织。\n先用 html 的 form 表单实现。如下：\n\u0026lt;form method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 通过 Post 提交 form 表单，Content-Type 默认是 application/x-www-form-urlencoded。\nGo 的实现代码：\ndata := make(url.Values) data.Set(\u0026#34;username\u0026#34;, \u0026#34;poloxue\u0026#34;) data.Set(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;) // 按 urlencoded 组织数据 body, _ := data.Encode() // 创建请求并设置内容类型 request, _ := http.NewRequest( http.MethodPost, \u0026#34;http://httpbin.org/post\u0026#34;, bytes.NewReader(body), ) request.Header.Set( \u0026#34;content-type\u0026#34;, \u0026#34;application/x-www-form-urlencoded\u0026#34;, ) http.DefaultClient.Do(request) 回想下前面说的三个步骤，组织请求体数据、设置 Content-Type 和发送请求。\nGo 的 net/htp 包还提供了一个更简洁的写法，http.Post。\nhttp.Post( \u0026#34;http://httpbin.org/post\u0026#34;, \u0026#34;application/x-www-form-urlencoded\u0026#34;, bytes.NewReader(body), ) 上传文件 RFC 1867 # 文件上传的需求很常见，但默认的 form 表单提交方式并不支持。\n如果是单文件上传，通过 body 二进制流就可以实现。但如果是一些更复杂的场景，如上传多文件，则需要自定义上传协议，而且客户端和服务端都要提供相应的支持。\n文件上传这种常见需求，如果有一套标准岂不更好。为了解决这个问题，RFC 1867 就诞生了，它主要内容有：\ninput 标签的类型增加一个 file 选项； form 表单的 enctype 增加 multipart/form-data 选项； 如下是一个支持文件提交的 form 表单。\n\u0026lt;form action=\u0026#34;http://httpbin.org/post\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34; \u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;words\u0026#34;/\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;uploadfile1\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;uploadfile2\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 提交表单后，将会看到请求的内容大致形式，如下：\nPOST http://httpbin.org/post HTTP/1.1 Content-Type: multipart/form-data; boundary=285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 multipart/form-data; boundary=285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 Content-Disposition: form-data; name=\u0026#34;uploadFile1\u0026#34;; filename=\u0026#34;uploadfile1.txt\u0026#34; Content-Type: application/octet-stream upload file1 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 Content-Disposition: form-data; name=\u0026#34;uploadFile1\u0026#34;; filename=\u0026#34;uploadfile2.txt\u0026#34; Content-Type: application/octet-stream upload file2 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350 Content-Disposition: form-data; name=\u0026#34;words\u0026#34; 123 --285fa365bd76e6378f91f09f4eae20877246bbba4d31370d3c87b752d350-- 注：如果使用 chrome 浏览器的开发者工具，为了性能考虑，无法看到看到这部分内容。而且，如果提交的是二进制流，只是一串乱码，也没什么可看的。\nContent-Type 除了 multipart/form-data，还另外多了 boundary=xxx 的内容。boundary是边界的意思，相当于 application/x-www-form-urlencoded 方式中的 \u0026amp;，用于分隔不同 input 字段。boundary 之所以这么复杂，因为，一般的文本内容使用了 \u0026amp; 就能分离，但如果是文件流，\u0026amp; 可能和内容冲突，对边界的唯一性要求更高。\nmultipart/form-data 内容的详细格式就不介绍了。继续说如何用 Go 实现这个功能。\nGo 实现代码 # 如何使用 Go 实现文件上传？\n主体逻辑依然是组织数据、设置 Content-Type 和发送请求这三步。但这部分数据的组织比 form 表单的 urlencoded 的方式要复杂的多。\nGo 的简洁性这时就体现出来了，因为，标准库 mime/multipart 已经提供了非常好用的方法，无需自己手动组织。\n假设，现在要实现前面 form 表单的功能，即提交两个文件，uploadfile1、uploadfile2，和一个字段 words。\n首先，创建一个用于保存数据的 byte.Buffer 类型的变量，body，在它之上创建一个 multipart.Writer，用这个 writer 组织将要提交的数据。代码如下：\nbodyBuf := \u0026amp;bytes.Buffer{} writer := multipart.NewWriter(payloadBuf) 先组织文件内容，两个文件的组织逻辑相同，就以 uploadfile1 为例进行介绍。在 writer 之上创建一个 fileWriter，用于写入文件 uploadFile1 的内容，\nfileWriter, err := writer.CreateFormFile(\u0026#34;uploadFile1\u0026#34;, filename) 打开要上传的文件，uploadfile1，将文件内容拷贝到 fileWriter中，如下：\nf, err := os.Open(\u0026#34;uploadfile1\u0026#34;) ... io.Copy(fileWriter, f) 添加字段就非常简单了，假设设置 words 为 123，代码如下：\nwriter.WriteField(\u0026#34;words\u0026#34;, \u0026#34;123\u0026#34;) 完成所有内容设置后，一定要记得关闭 Writer，否则，请求体会缺少结束边界。\nwriter.Close() 完成了数据的组织。\n接下来，只要将数据设置到 http.Post 就好了。\nr, err := http.Post( \u0026#34;http://httpbin.org/post\u0026#34;, writer.FormDataContentType(), body, ) 完成了支持文件上传的表单提交。\n总结 # 本篇文章主要介绍了如何使用 Go 实现文件上传，本质上是组织提交文件的请求体。而为了能清晰地了解请求体的组织过程，就必须清楚相关的 HTTP 协议，rfc 1867。\n博文地址：Go 如何实现 HTTP 文件上传\n","date":"2019-12-10","externalUrl":null,"permalink":"/posts/2019-12-10-golang-http-upload-file/","section":"文章","summary":"早前写过一篇文章，Go HTTP 请求 QuickStart。当时，主要参考 Python 的 requests 大纲介绍 Go 的 net/http 如何发起 HTTP 请求。\n最近，尝试录成它的视频，访问地址。发现当时写得挺详细的，发现当时虽然写得比较详细，但也只是介绍用法，可能不知其所以然。比如文件上传那部分，如果不了解 http 文件上传协议 RFC 1867，就很难搞懂为什么代码这么写。\n","title":"Go 如何实现 HTTP 文件上传","type":"posts"},{"content":"和其他语言没有区别，Go 中的数据也是两种表示方式，常量和变量，本文先说说变量吧。\n为了增加文章的趣味性（多掉些头发），搜集了一些常见的面试题。部分是自己瞎编的，顺便为自己明年可能到来的面试做些准备。\n先答题，题目中附有提示，但无解答。带着问题看文章效果或许更好。\n面试题 # 1.1 如下的代码，哪些能正常编译？如果不能正常编译，如何修改？\nA.\npackage main import ( \u0026#34;fmt\u0026#34; ) func get() { return 1, 2 } func main() { x, y := get() fmt.Println(x) } 考点：定义未使用的局部变量和使用匿名变量。\nB.\npackage main import ( \u0026#34;fmt\u0026#34; ) var ( x = 1 y := 10 ) func main() { fmt.Println(x) } 考点：简短模式只能定义局部变量\nC.\npackage main import ( \u0026#34;fmt\u0026#34; ) var i int, s string = 1, \u0026#34;3\u0026#34; func main() { fmt.Println(i, s) } 考点：var 定义多个变量\n1.2 下面这段代码逻辑是否正确？\npackage main import ( \u0026#34;fmt\u0026#34; ) var p *int func foo() (*int, error) { var i int = 5 return \u0026amp;i, nil } func bar() { // 使用 p fmt.Println(*p) } func main() { p, err := foo() if err != nil { fmt.Println(err) return } bar() fmt.Println(*p) } 考点：变量的作用域问题\n注：取自 tonybai 老师的博客，原文地址。\n1.3 下面哪一行变量简短定义存在已定义变量的赋值行为？\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { x := 1 fmt.Println(\u0026amp;x) if x, y := 3, 4; true { x = x + y fmt.Println(\u0026amp;x) } x , y := 5, 6 x = x + y fmt.Println(\u0026amp;x) } 考点：局部变量的作用域与简单模式的退化赋值。\n题目是不是都非常简单呢？如有问题，继续看正文吧。\n什么是变量 # 变量是可以理解为使用一个名称绑定一个用来存放数据的内存块。\n变量，首先是量，即为数据，而后是变，即表示内存块中的数据是可变的。与变量相对的是常量，常即恒常，表示数据不可变。常量的值是在编译期就确定了。\n变量的定义 # Go 中变量的定义有多种方式，先看一个变量完整的定义组成。如下：\n变量名称 变量类型 变量值 var varName typeName [= Value] var 是 Go 提供的用于定义变量的关键词，变量的定义语句可出现在函数和包级别中。\n语句中核心是三个部分，分别是变量的名称、类型和值。与 C/C++ 不同，Go 的变量类型是在变量名称之后。\n定义一个变量：\nvar i int var 除了定义单个变量，还可以一次定义多个变量。\n// 相同类型简写 var i, j int // 定义不同类型变量 var ( i int s string ) 初始化 # 变量定义时可以指定初始值。\nvar i int = 1 var f float64 = 1.1 var s string = \u0026#34;string\u0026#34; 变量值的可选范围由变量类型决定。Go 是静态语言，变量类型是不可修改的。\nvar i int var s string 如果变量定义时，没有指定初始值，将自动初始化为相应的零值（不同类型，零值不同），避免类似 C/C++ 中不可预测的行为。\n在 windows 上的 \u0026ldquo;烫烫烫\u0026rdquo; 的梗，就是和变量未初始化有关。\n不同类型变量的零值如下：\n数值类型：0； 布尔类型：false； 字符串类型：空字符串； 接口或引用类型（slice、指针、map、chan 和 函数）： nil； 聚合类型（数组、结构体）：每个元素或字段都为对应类型的零值； 如果定义时，指定初始值，则可以省略类型，Go 编译器会自动推导变量类型。\nvar i = 1 // 同时定义初始化两个不同类型变量 var f, s = 1.1, \u0026#34;string\u0026#34; 简短定义 # 在函数中，变量的定义有一种简短写法，:=。在初始化值类型明确的情况下，代替 var，实现类似动态语言的效果，懒人神器。\ni := 1 变量类型由编译器根据初始化值自动推导。\n要注意的是，函数外的每个语句都必须以关键字开始（var, func 等），简短模式不能在函数外使用。\n简短模式下，如果语句左边有多个变量，其中包含已定义变量，且必须是位于当前的作用域，则已定义变量会转化为赋值行为。\nvar x = 1 fmt.Println(\u0026amp;x, x) x, y := 10, 20 fmt.Println(\u0026amp;x, x) 运行代码将会发现，x 的值修改了，但地址并未改变。\n多变量赋值 # 定义变量时，已经演示了如何同时为多个变量赋初始值。动态语言通常支持这种写法，比如 Pyhon。\nx, y := 10, 20 x, y = x+10, y+20 这种语法在简化写法的同时，还有一个比较有用的点，变量交换。\n通常，交换变量的写法：\nt := x x = y y = t 引入一个临时变量实现交换。除此之外，还有两种比较常见的交换算法，不引入临时变量。\nx = x + y y = x - y x = x - y 或者\na = a^b b = b^a a = a^b 有了多变量同时赋值的特性之后，如下的写法即可完成交换。\nx, y = y, x 匿名变量 # Go 语言中会将定义但未使用的变量当成错误。但是有一种情况，如果 Go 的函数允许返回多个值，就要定义多个变量接收。\n假设，有函数定义如下：\nfunc row() (string, int) { return \u0026#34;poloxue\u0026#34;, 18 } 现在 main 函数将打印第一个返回值，第二个返回值不会使用。\nfunc main() { name, age := row() fmt.Println(name) } 编译无法通过，提示存在未使用的变量。\n这时，可以使用 Go 中提供的匿名变量 _ 接收无用的返回值。\nname, _ := row() 匿名变量有什么特点？匿名变量可以多次使用，不占用内存空间，解决未使用变量的报错问题。\n变量作用域 # 变量作用域和生命周期不同，生命周期表示变量执行期间的存活时间，而作用域表示变量能有效使用的范围。\n除了变量有作用范围，还有诸如常量、函数、类型等都是有作用域的。\nGo 的作用域可分为全局和局部，变量也就有全局变量和局部变量。但细究起来，全局和局部变量的说法也不对，Go 中内置的常量、函数、类型才能算是全局。变量的全局只能算包级别，包级别变量支持访问控制，变量名首字母大写，才能在全局可用。\n局部会覆盖全局，不同的作用范围可以重新定义同名变量覆盖上一级作用域的变量。\npackage main // 全局变量 var i = 1 func printI() { // 局部变量覆盖全局变量 i := 10 fmt.Println(i) } func main() { fmt.Println(i) printI() } 局部变量有几种情况，分别是函数的参数与返回值，函数体内部定义变量，函数内部语法块等。\n函数体内部作用域的例子。\nfunc main() { s := \u0026#34;局部变量\u0026#34; { s := \u0026#34;语法块内部变量\u0026#34; fmt.Println(s) } fmt.Println(s) } 变量作用域是一个很坑的话题，Go 中每个语法块，如 func、if、for、select、switch 等，都有一个隐式的作用域。基于它，出现了很多坑死人不偿命的面试题。\n一个简单的例子。\nfunc get() int { return 1 } func main() { if x := get(); x == 0 { fmt.Println(x) } else { fmt.Println(x) } } else 中也可以使用 x 变量，if 之上有个隐式作用域。\n作用域这块还有很多坑，比如与 defer 结合就会产生更多叹为观止（惨绝人寰）的现象。\n","date":"2019-12-05","externalUrl":null,"permalink":"/posts/2019-12-05-golang-variables/","section":"文章","summary":"和其他语言没有区别，Go 中的数据也是两种表示方式，常量和变量，本文先说说变量吧。\n为了增加文章的趣味性（多掉些头发），搜集了一些常见的面试题。部分是自己瞎编的，顺便为自己明年可能到来的面试做些准备。\n","title":"说说 Go 中的变量（附粗制滥造面试题）","type":"posts"},{"content":"开源库 Cobra 是 Golang 下用于创建命令行应用的框架。 它也是 k8s、hugo 等开源项目都在用框架。GitHub 项目地址\n命令行应用 # 对于 Golang 而言，一般写命令行应用，如果要求不是太多，直接用 flag 标准库就够了，毕竟像 Go 命令也是通过 flag 包实现的，完全能够驾驭。前段时间写了篇文章：Go 命令行解析 flag 包之通过子命令实现看 go 命令源码\n命令行应用可以和 web 应用做类比，就像 web 只有路由，就可以支持实现一个复杂的 web 应用，同样 flag 提供的基本功能，也足够写出足够复杂的命令行应用了。\n但不是任何人都想去了解 flag，或者裸写一个命令行应用。\n为什么呢？\n因为命令行的有些内容还要处理，如，默认没有子命令的一套实现规范，不支持参数校验、不支持帮助信息模板配置。没有对一些标准提供默认支持，如 POSIX 标准，没有 -v，\u0026ndash;version 和 -h、\u0026ndash;help 的默认支持等。\n一套完整的命令行框架应用就要这些能力，能提供一套最佳实践。Cobra 就是这样一套框架。\nCobra # Cobra 支持的功能非常多，一些非常出名的开源项目，比如 k8s、hugo 等都在使用它。GitHub 的说明已经介绍了它丰富的能力。\n概述而言，它支持子命令，posix 规范的 Flag，嵌套的子命令，支持全局、局部和级联的选项，支持 bash 自动补全，便捷的参数校验。具体可查看 GitHub README，或\n它还提供了一套脚手架，能便捷地创建一个命令行应用，就像写 web 应用一样，快速创建一个 handler、Controller。\n当然，如果只是写个简单小工具，连子命令都没有，Flag 选项又少的可怜。flag 就足够了，没有必要依赖它。\n使用演示 # 使用 Cobra 创建一个简单的命令行应用，官方文档案例。\n安装 # 博文地址：Golang 开源库 Cobra 介绍与使用，知乎问题：如何评价 Cobra （Golang 库）？\n","date":"2019-12-03","externalUrl":null,"permalink":"/posts/2019-12-03-what-is-cobra/","section":"文章","summary":"开源库 Cobra 是 Golang 下用于创建命令行应用的框架。 它也是 k8s、hugo 等开源项目都在用框架。GitHub 项目地址\n命令行应用 # 对于 Golang 而言，一般写命令行应用，如果要求不是太多，直接用 flag 标准库就够了，毕竟像 Go 命令也是通过 flag 包实现的，完全能够驾驭。前段时间写了篇文章：Go 命令行解析 flag 包之通过子命令实现看 go 命令源码\n","title":"如何评价 Golang 开源库 Cobra","type":"posts"},{"content":"上篇文章 介绍了 flag 中如何扩展一个新的类型支持。本篇介绍如何使用 flag 实现子命令，总的来说，这篇才是这个系列的核心，前两篇只是铺垫。\n前两篇文章链接如下：\nGo 命令行解析 flag 包之快速上手\nGo 命令行解析 flag 包之扩展新类型\n希望看完本篇文章，如果再阅读 go 命令的实现源码，至少在整体结构上不会迷失方向了。\nFlagSet # 正式介绍子命令的实现之前，先了解下 flag 包中的一个类型，FlagSet，它表示了一个命令。\n从命令的组成要素上看，一个命令由命令名、选项 Flag 与参数三部分组成。类似如下：\n$ cmd --flag1 --flag2 -f=flag3 arg1 arg2 arg3 FlagSet 的定义也正符合了这一点，如下：\ntype FlagSet struct { // 打印命令的帮助信息 Usage func() // 命令名称 name string parsed bool // 实际传入的 Flag actual map[string]*Flag // 会被使用的 Flag，通过 Flag.Var() 加入到了 formal 中 formal map[string]*Flag // 参数，Parse 解析命令行传入的 []string， // 第一个不满足 Flag 规则的（如不是 - 或 -- 开头）， // 从这个位置开始，后面都是 args []string // arguments after flags // 发生错误时的处理方式，有三个选项，分别是 // ContinueOnError 继续 // ExitOnError 退出 // PanicOnError panic errorHandling ErrorHandling output io.Writer // nil means stderr; use out() accessor } 包含字段有命令名 name，选项 Flag 有 formal 和 actual，参数 args。\n如果有人说，FlagSet 是命令行实现的核心，还是比较认同的。之所以前面一直没有提到它，主要是 flag 包为了简化命令行的处理流程，在 FlagSet 上做了进一步的封装，简单的使用可以直接无视它的存在。\nflag 中定义了一个全局的 FlagSet 类型变量，CommandLine，用它表示整个命令行。可以说，CommandLine 是 FlagSet 的一个特例，它的使用模式较为固定，所以在它之上能提供了一套默认的函数。\n前面已经用过的一些，比如下面这些函数。\nfunc BoolVar(p *bool, name string, value bool, usage string) { CommandLine.Var(newBoolValue(value, p), name, usage) } func Bool(name string, value bool, usage string) *bool { return CommandLine.Bool(name, value, usage) } func Parse() { // Ignore errors; CommandLine is set for ExitOnError. CommandLine.Parse(os.Args[1:]) } 更多的，这里不一一列举了。\n接下来，我们来脱掉这层外衣，梳理下命令行的整个处理流程吧。\n流程解读 # CommandLine 的整个使用流程主要由三部分组成，分别是获取命令名称、定义命令中的实际选项和解析选项。\n命令名称在 CommandLine 创建的时候就已经指定了，如下：\nCommandLine = NewFlagSet(os.Args[0], ExitOnError) 名称由 os.Args[0] 指定，即命令行的第一个参数。除了命令名称，同时指定的还有出错时的处理方式，ExitOnError。\n接着是定义命令中实际会用到的 Flag。\n核心的代码是 FlagSet.Var()，如下所示：\nfunc (f *FlagSet) Var(value Value, name string, usage string) { // Remember the default value as a string; it won\u0026#39;t change. flag := \u0026amp;Flag{name, usage, value, value.String()} // ... // 省略部分代码 // ... if f.formal == nil { f.formal = make(map[string]*Flag) } f.formal[name] = flag } 之前使用过的 flag.BoolVar 和 flag.Bool 都是通过 CommandLine.Var()，即 FlagSet.Var()， 将 Flag 保存到 FlagSet.formal 中，以便于之后在解析的时候能将值成功设置到定义的变量中。\n最后一步是从命令行中解析出选项 Flag。由于 CommandLine 表示的是整个命令行，所以它的选项和参数一定是从 os.Args[1:] 中解析。\nflag.Parse 的代码如下：\nfunc Parse() { // Ignore errors; CommandLine is set for ExitOnError. CommandLine.Parse(os.Args[1:]) } 现在的重点是要了解 flag 中选项和参数的解析规则，如 gvg -v list，按什么规则确定 -v 是一个 Flag，而 list 是参数的呢？\n如果继续向下追 Parse 的源码，在 FlagSet.parseOne 中将发现 Flag 的解析规则。\nfunc (f *FlagSet) ParseOne() if len(f.args) == 0 { return false, nil } s := f.args[0] if len(s) \u0026lt; 2 || s[0] != \u0026#39;-\u0026#39; { return false, nil } numMinuses := 1 if s[1] == \u0026#39;-\u0026#39; { numMinuses++ if len(s) == 2 { // \u0026#34;--\u0026#34; terminates the flags f.args = f.args[1:] return false, nil } } // ... } 三种情况下会终止解析 Flag，分别是当命令行参数全部解析结束，即 len(f.args) == 0，或长度小于 2，但第一位字符不是 -，或者参数长度等于 2，且第二个字符是 -。之后的内容会继续当作命令行参数处理。\n如果没有子命令，命令的解析工作到此就基本完成了，再往后就是业务代码的开发了。那如果 CommandLine 还有子命令呢？\n子命令 # 子命令和 CommandLine 无论是形式还是逻辑上，基本没什么差异。形式上，子命令同样包含选项和参数，逻辑上，子命令的选项和参数的解析规则与 CommandLine 相同。\n一个包含子命令的命令行，形式如下：\n$ cmd --flag1 --flag2 subcmd --subflag1 --subflag2 arg1 arg2 从上面可以看出，如果 CommandLine 包含了子命令，可以理解为本身也就没了参数，因为 CommandLine 的第一个参数即是子命令的名称，而之后的参数要解析为子命令的选项参数了。\n现在，子命令的实现就变得非常简单了，创建一个新的 FlagSet，将 CommandLine 中的参数按前面介绍的流程重新处理一下。\n第一步，获取 CommandLine.Arg(0)，检查是否存在相应的子命令。\nfunc main() { flag.Parse() if h { flag.Usage() return } cmdName := flag.Arg(0) switch cmdName { case \u0026#34;list\u0026#34;: _ = list.Exec(cmdName, flag.Args()[1:]) case \u0026#34;install\u0026#34;: _ = install.Exec(cmdName, flag.Args()[1:]) } } 子命令的实现定义在另外一个包中，以 list 命令为例。 代码如下：\nvar flagSet *flag.FlagSet var origin string func init() { flagSet = flag.NewFlagSet(\u0026#34;list\u0026#34;, flag.ExitOnError) val := newStringEnumValue(\u0026#34;installed\u0026#34;, \u0026amp;origin, []string{\u0026#34;installed\u0026#34;, \u0026#34;local\u0026#34;, \u0026#34;remote\u0026#34;}) flagSet.Var( val, \u0026#34;origin\u0026#34;, \u0026#34;the origin of version information, such as installed, local, remote\u0026#34;, ) } 上面的代码中，定义了 list 子命令的 FlagSet，并在 Init 方法为其增加了一个选项 Flag，origin。\nRun 函数是真正执行业务逻辑的代码。\nfunc Run(args []string) error { if err := flagSet.Parse(args); err != nil { return err } fmt.Println(\u0026#34;list --oriign\u0026#34;, origin) return nil } 最后的 Exec 函数组合 Init 和 Run 函数，已提供给 main 调用。\nfunc Run(name string, args []string) error { Init(name) if err := Run(args); err != nil { return err } return nil } 命令行的解析完成，如果子命令还有子命令，处理的逻辑依然相同。接下来的工作，就可以开始在 Run 函数中编写业务代码了。\nGo 命令 # 现在，阅读下 Go 命令的实现代码吧。\n由于大佬们写的代码是基于 flag 包实现纯手工打造，没用任何的框架，在可读性上会有点差。\n源码位于 go/src/cmd/go/cmd/main.go 下，通过 base.Go 变量初始化了 Go 支持的所有命令，如下：\nbase.Go.Commands = []*base.Command{ bug.CmdBug, work.CmdBuild, clean.CmdClean, doc.CmdDoc, envcmd.CmdEnv, fix.CmdFix, fmtcmd.CmdFmt, generate.CmdGenerate, modget.CmdGet, work.CmdInstall, list.CmdList, modcmd.CmdMod, run.CmdRun, test.CmdTest, tool.CmdTool, version.CmdVersion, vet.CmdVet, help.HelpBuildmode, help.HelpC, help.HelpCache, help.HelpEnvironment, help.HelpFileType, modload.HelpGoMod, help.HelpGopath, get.HelpGopathGet, modfetch.HelpGoproxy, help.HelpImportPath, modload.HelpModules, modget.HelpModuleGet, modfetch.HelpModuleAuth, modfetch.HelpModulePrivate, help.HelpPackages, test.HelpTestflag, test.HelpTestfunc, } 无论是 go 命令，还是它的子命令，都是 *base.Command 类型。可以看一下 *base.Command 的定义。\ntype Command struct { Run func(cmd *Command, args []string) UsageLine string Short string Long string Flag flag.FlagSet CustomFlags bool Commands []*Command } 主要的字段有三个，分别是 Run，主要负责业务逻辑的处理，FlagSet，负责命令行的解析，以及 []*Command， 所支持的子命令。\n再来看看 main 函数中的核心逻辑。如下：\nBigCmdLoop: for bigCmd := base.Go; ; { for _, cmd := range bigCmd.Commands { // ... // 主要逻辑代码 // ... } // 打印帮助信息 helpArg := \u0026#34;\u0026#34; if i := strings.LastIndex(cfg.CmdName, \u0026#34; \u0026#34;); i \u0026gt;= 0 { helpArg = \u0026#34; \u0026#34; + cfg.CmdName[:i] } fmt.Fprintf(os.Stderr, \u0026#34;go %s: unknown command\\nRun \u0026#39;go help%s\u0026#39; for usage.\\n\u0026#34;, cfg.CmdName, helpArg) base.SetExitStatus(2) base.Exit() } 从最顶层的 base.Go 开始，遍历 Go 的所有子命令，如果没有相应的命令，则打印帮助信息。\n省略的那段主要逻辑代码如下：\nfor _, cmd := range bigCmd.Commands { // 如果找不到命令，继续下次循环 if cmd.Name() != args[0] { continue } // 检查是否存在子命令 if len(cmd.Commands) \u0026gt; 0 { // 将 bigCmd 设置为当前的命令 // 比如 go tool compile，cmd 即为 compile bigCmd = cmd args = args[1:] // 如果没有命令参数，则说明不符合命令规则，打印帮助信息。 if len(args) == 0 { help.PrintUsage(os.Stderr, bigCmd) base.SetExitStatus(2) base.Exit() } // 如果命令名称是 help，打印这个命令的帮助信息 if args[0] == \u0026#34;help\u0026#34; { // Accept \u0026#39;go mod help\u0026#39; and \u0026#39;go mod help foo\u0026#39; for \u0026#39;go help mod\u0026#39; and \u0026#39;go help mod foo\u0026#39;. help.Help(os.Stdout, append(strings.Split(cfg.CmdName, \u0026#34; \u0026#34;), args[1:]...)) return } // 继续处理子命令 cfg.CmdName += \u0026#34; \u0026#34; + args[0] continue BigCmdLoop } if !cmd.Runnable() { continue } cmd.Flag.Usage = func() { cmd.Usage() } if cmd.CustomFlags { // 解析参数和选项 Flag // 自定义处理规则 args = args[1:] } else { // 通过 FlagSet 提供的方法处理 base.SetFromGOFLAGS(cmd.Flag) cmd.Flag.Parse(args[1:]) args = cmd.Flag.Args() } // 执行业务逻辑 cmd.Run(cmd, args) base.Exit() return } 主要是几个部分，分别是查找命令，检查是否存在子命令，选项和参数的解析，以及最后是命令的执行。\n通过 cmd.Name() != args[0] 判断是否查找到了命令，如果找到则继续向下执行。\n通过 len(cmd.Commands) 检查是否存在子命令，存在将 bigCmd 覆盖，并检查是否符合命令行是否符合规范，比如检查 len(args[1:]) 如果为 0，则说明传入的命令行没有提供子命令。如果一切就绪，通过 continue 进行下一次循环，执行子命令的处理。\n接着是命令选项和参数的解析。可以自定义处理规则，也可以直接使用 FlagSet.Parse 处理。\n最后，调用 cmd.Run 执行逻辑处理。\n总结 # 本文介绍了 Go 中如何通过 flag 实现子命令，从 FlagSet 这个结构体讲起，通过 flag 包中默认提供的 CommandLine 梳理了 FlagSet 的处理逻辑。在基础上，实现了子命令的相关功能。\n本文最后，分析了 Go 源码中 go 如何使用 flag 实现。因为是纯粹使用 flag 包裸写，读起来稍微有点难度。本文只算是一个引子，至少帮助大家在大的方向不至于迷路，里面更多的细节还需要自己挖掘。\n博文地址：Go 命令行解析 flag 包之通过子命令实现看 go 命令源码\n","date":"2019-11-30","externalUrl":null,"permalink":"/posts/2019-11-30-golang-flag-sub-commandline/","section":"文章","summary":"上篇文章 介绍了 flag 中如何扩展一个新的类型支持。本篇介绍如何使用 flag 实现子命令，总的来说，这篇才是这个系列的核心，前两篇只是铺垫。\n前两篇文章链接如下：\nGo 命令行解析 flag 包之快速上手\nGo 命令行解析 flag 包之扩展新类型\n","title":"Go 命令行解析 flag 包之通过子命令实现看 go 命令源码","type":"posts"},{"content":"上篇文章 说到，flag 支持的类型有布尔类型、整型（int、int64、uint、uint64）、浮点型（float64）、字符串（string）和时长（duration）。\n一般情况下，flag 内置类型能满足绝大部分的需求，但某些场景，我们要自定义解析规则。一个优秀的库肯定要支持扩展的。\n本文将介绍如何为 flag 扩展一个新的类型支持？\n扩展目标 # 上文中，假设我们在开发一个 gvg 命令行工具。它其中的 list 子命令支持获取 Go 的版本列表。但 Go 版本来源信息有多处，比如 installed（已安装）、local（本地仓库）和 remote（远程仓库）。\n查看下 list 的帮助信息，如下：\nNAME: gvg list - list go versions USAGE: gvg list [command options] [arguments...] OPTIONS: --origin value the origin of version information , such as installed, local, remote (default: \u0026#34;installed\u0026#34;) 从帮助信息中可知，list 支持一个 Flag 选项，--origin。它用于指定版本信息的来源，允许值的范围是 installed、local 和 remote。\n如果要求不严格，用 StringVar 也可以实现。但问题是，使用 String，即使输入不在指定范围也能成功解析，不够严谨。虽说在获取后也可以检查，但还是不够灵活、可配置型也差。\n接下来，我们要实现一个新的类型的 Flag，使选项的值必需在指定范围，否则要给出一定的错误提示信息。\n实现思路 # 如何展一个新类型呢？\n参考 flag 包内置类型的实现思路，如 flag.DurationVar。Duration 不是基础类型，解析结果是存放到了 time.Duration 类型中，可能有参考价值。\n我们进入 flag.DurationVar 查看源码，如下：\nfunc DurationVar(p *time.Duration, name string, value time.Duration, usage string) { CommandLine.Var(newDurationValue(value, p), name, usage) } 通过 newDurationValue 创建了一个类型为 durationValue 的变量，并传入到了 CommandLine.Var 方法中。\n如果继续往下追，会根据 Value 创建一个 Flag 变量。 如下：\nfunc (f *FlagSet) Var(value Value, name string, usage string) { flag := \u0026amp;Flag{name, usage, value, value.String()} ... } 从 Var 的定义可以看出，它的第一个参数类型是 Value 接口类型，也就说，durationValue 是实现了 Value 接口的类型。\n注意，源码中出现的 FlagSet 可以先忽略，它是下篇介绍子命令时重点关注的对象。\n看下 Value 的定义，如下：\ntype Value interface { String() string Set(string) error } 那么，durationValue 的实现代码如何？\n// 传入参数分别是默认值和获取 Flag 值的变量地址 func newDurationValue(val time.Duration, p *time.Duration) *durationValue { // 将默认值设置到 p 上 *p = val // 使用 p 创建新的类型，保证可以获取到解析的结果 return (*durationValue)(p) } // Set 方法负责解析传入的值 func (d *durationValue) Set(s string) error { v, err := time.ParseDuration(s) if err != nil { err = errParse } *d = durationValue(v) return err } // 获取真正的值 func (d *durationValue) String() string { return (*time.Duration)(d).String() } 核心在两个地方。\n一个是创建新类型变量时，要使用传入的变量地址创建新类型变量，以实现将解析结果放到其中，让前端能获取到，二是 Set 方法中实现命令行传入字符串的解析。\n逻辑梳理 # 看完上个小节，基本已经了解如何扩展一个新类型了。本质是是实现 Value 接口。\n再看下之前提到的几个变量，分别是存放解析结果的指针、解析命令行输入的 Value 和表示一个选项的 Flag。对应于 flag.DurationVar，这个变量的类型分别是 *time.Duration、durationValue 和 Flag。\n比如有 duration=1h，大致流程是首先从 os.Args 获取参数，按规则解析出选项名称 duration，查找是否存在名称为 duration 的 Flag，如果存在，使用 Flag.Value.Set 解析 1h，如果不满足 duration 的要求，将给出错误提示。\n实现新类型 # 现在实现文章开头要求的目标。\n新类型定义如下：\ntype stringEnumValue struct { options []string p *string } 我们创建了新类型 - stringEnumValue，即字符串枚举。它有 options 和 p 两个成员，options 指定一定范围的值，p 是 string 指针，保存解析结果的变量的地址。\n下面定义创建 StringEnumValue 变量的函数 newStringEnumValue，代码如下：\nfunc newStringEnumValue(val string, p *string, options []string) *StringEnumValue { *option = val return \u0026amp;stringEnumValue{options: options, p: p} } 除了 val 和 p 两个必要的输入外，还有一个 string 切片类型的数，名为 options，它用于范围的限定。而函数主体，首先设置默认值，然后使用 options 和 p 创建变量返回。\nSet 是核心方法，解析命令行传入字符串。代码如下：\nfunc (s *StringEnumValue) Set(v string) error { for _, option := range s.options { if v == option { *(s.p) = v return nil } } return fmt.Errorf(\u0026#34;must be one of %v\u0026#34;, s.options) } 循环检查输入参数 v 是否满足要求。定义如下：\n最后是 String() 方法，\nfunc (s *StringEnumValue) String() string { return *(s.p) } 返回 p 指针中的值。前面分析实现思路时，Flag 在设置默认值时就调用了它。\n使用 StringEnumValue # 直接看代码吧。如下：\nvar origin string func init() { flag.Var( newStringEnumValue( \u0026#34;installed\u0026#34;, // 默认值 \u0026amp;origin, []string{\u0026#34;installed\u0026#34;, \u0026#34;local\u0026#34;, \u0026#34;remote\u0026#34;}, ), \u0026#34;origin\u0026#34;, `the origin of version information, such as installed, local, remote (default: \u0026#34;installed\u0026#34;)`, ) } func main() { flag.Parse() fmt.Println(option) } 重点就是 flag.Var(newStringEnumValue(...)，...)。如果觉得有点啰嗦，希望和其他类型新建过程相同，在这个基础上可以再包装。代码如下：\nfunc StringEnumVar(p *string, name string, options []string, defVal string, usage string) { flag.Var(newStringEnumValue(defVal, p, options), name, usage) } 编译测试下，结果如下：\n$ gvg --origin=any invalid value \u0026#34;any\u0026#34; for flag -origin: must be one of [installed local remote] Usage of gvg: -origin value the origin of version information, such as installed, local, remote (default installed) $ gvg --origin=remote origin remote 总结 # 本文介绍了如何为 flag 扩展一个类型支持，通过分析源码理清实现思路。最后创建了一个只接收指定范围值的 Value。\n博文地址：Go 命令行解析 flag 包之扩展新类型\n","date":"2019-11-26","externalUrl":null,"permalink":"/posts/2019-11-26-commandline-flag-extend-new-type/","section":"文章","summary":"上篇文章 说到，flag 支持的类型有布尔类型、整型（int、int64、uint、uint64）、浮点型（float64）、字符串（string）和时长（duration）。\n","title":"Go 命令行解析 flag 包之扩展新类型","type":"posts"},{"content":"本篇文章是 Go 标准库 flag 包的快速上手篇。\n概述 # 开发一个命令行工具，视复杂程度，一般要选择一个合适的命令行解析库，简单的需求用 Go 标准库 flag 就够了，flag 的使用非常简单。\n当然，除了标准库 flag 外，也有不少的第三方库。比如，为了替代 flag 而生的 pflag，它支持 POSIX 风格的命令行解析。关于 POSIX 风格，本文末尾有个简单的介绍。\n更多与命令行处理相关的库，可以打开 awesome-go#command-line 命令行一节查看，star 最多的是 spf13/cobra 和 urfave/cli ，与 flag / pflag 相比，它们更加复杂，是一个完全的全功能的框架。\n有兴趣都可以了解下。\n目标案例 # 回归主题，继续介绍 flag 吧。通过案例介绍包的使用会比较直观。\n举一个例子说明吧。假设，现在要开发一个 Go 语言环境的版本管理工具，gvg（go version management using go）。\n命令行的帮助信息如下：\nNAME: gvg - go version management by go USAGE: gvg [global options] command [command options] [arguments...] VERSION: 0.0.1 COMMANDS: list list go versions install install a go version info show go version info use select a version uninstall uninstall a go version get get the latest code uninstall uninstall a go version help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help --version, -v print the version 这个命令不仅包含了全局的选项，还有 8 个子命令，部分子命令支持参数和选项。暂时，子命令的选项参数先不列出来了，实现时再看。\n接下来，我们试着通过 flag 实现这个效果。本文只介绍 GLOBAL OPTIONS（全局选项）的实现。\n如果想了解什么是 Go 语言环境的版本管理，可以查看 如何灵活地进行 Go 版本管理 一文。\n选项表示 # 最简单的命令不需要任何参数和选项，复杂一点，要支持参数和选项的配置。gvg 没有全局参数，或者说全局参数是子命令，全局选项有 --help -h 和 --version -h。\n一个选项在 flag 包中用一个 Flag 表示，那 -h 可以用一个 Flag 表示。一个选项通常由几个部分组成，如名称、使用说明和默认值。如果将 -h 用代码表示，如下：\nh := flag.Bool(\u0026#34;h\u0026#34;, false, \u0026#34;show help\u0026#34;) 定义了一个布尔类型的 Flag，名为 h，默认值是 false，使用说明为 \u0026ldquo;show help\u0026rdquo;。变量 h 是一个布尔型的指针，通过它可以取出命令行传入的值。\n除了使用 flag.Bool，还可以使用另外一种方式，Flag.BoolVar 定义一个 Flag。我们可以用这种方式定义 -v 选项。\n代码如下：\nvar v bool flag.BoolVar(\u0026amp;v, \u0026#34;v\u0026#34;, false, \u0026#34;print the version\u0026#34;) 最后的三个参数含义与 flag.Bool 相同，主要区别在值的获取方式，flag.BoolVar 是通过将变量地址传入获取值。从经验来看，第二种方式使用的较多，或许因为第一种方式会发生变量逃逸。\n更多类型 # 除了布尔类型，Flag 的类型还有整数（int、int64、uint、uint64）、浮点数（float64）、字符串（string）和时长（time.Duration）。\n假设 gvg 的案例中，支持配置文件选项 --config-path。实现代码如下：\nvar configPath flag.StringVar(\u0026amp;configPath, \u0026#34;config-path\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;config file path\u0026#34;) 通过 StringVar 定义了新的 Flag。使用方式与 BoolVar 相同，最后的三个参数分别是选项名称、默认值和使用说明。\n虽然 flag 支持的内置类型并不多，但已经满足大部分需求了。如果有自定义的需求，也可以扩展新的类型实现，这部分内容下篇介绍。\n长短选项 # 现在已经完成了 -h 和 -v 两个选项，但目标是 -v --version 和 -h --help，即同时支持长短选项。\n一个 Flag 应该有长短两种形式，但 flag 包并不支持这种风格，需要曲线救国才能实现。（注：本文开开头提到的 pflag 支持。）\n这里以 -v --version 为例，代码如下：\nflag.BoolVar(\u0026amp;v, \u0026#34;v\u0026#34;, false, \u0026#34;print the version\u0026#34;) flag.BoolVar(\u0026amp;v, \u0026#34;version\u0026#34;, false, \u0026#34;print the version\u0026#34;) 定义了两个 Flag，同时绑定到了一个变量上。这种效果只能用 flag.BoolVar 方式定义新的 Flag，flag.Bool 没办法做到将同一个变量同时绑定两个 Flag。\n但其实这种也有缺点，先不说了，后面介绍帮助信息打印时就明白了。\n命令行解析 # 定义好所有 Flag，还需要一步解析才能拿到正确的结果。这一步非常简单，调用 flag.Parse() 即可。\n如下是完整的代码：\npackage main var h *bool var v bool func init() { flag.BoolVar(\u0026amp;h, \u0026#34;h\u0026#34;, false, \u0026#34;show help\u0026#34;) flag.BoolVar(\u0026amp;h, \u0026#34;help\u0026#34;, false, \u0026#34;show help\u0026#34;) flag.BoolVar(\u0026amp;v, \u0026#34;v\u0026#34;, false, \u0026#34;print the version\u0026#34;) flag.BoolVar(\u0026amp;v, \u0026#34;version\u0026#34;, false, \u0026#34;print the version\u0026#34;) } func main() { flag.Parse() fmt.Println(\u0026#34;version\u0026#34;, v) fmt.Println(\u0026#34;help\u0026#34;, h) } 通过 flag.Parse() 解析完成，打印下 v 和 h 变量，确认下是否成功获取到了值。\n到此，代码就告一段落了，现在将它编译为 gvg 命令吧。\n使用命令 # 在正式使用命令前，先介绍下 flag 的语法。官方文档说明，命令行中 flag 选项的使用语法有如下几种形式。\n-flag -flag=x -flag x // 非布尔类型才支持这种方式 但其实，\u0026ndash; 也是支持的。因此，上面才可以实现 --version 的曲线救国。\n使用下这个命令，将 help 设置为 false 和 version 设置为 true。我尽量把所有可能的写法都列出来。\n$ gvg -v $ gvg -version -h=false # 单个 - ，即 -version 支持 $ gvg --version=true --help=false $ gvg --version=1 --help=0 $ gvg --version=t --help=f $ gvg --version=T --help=F $ gvg --version true --help true # 写法错误，因为无法识别出是 bool 值，还是参数或子命令 $ gvg -vh # 不支持这种风格 执行命令，输出结果：\nversion true help false 到这里，flag 的快速入门就介绍完了。参数留在子命令的时候介绍。\n命令行风格 # 由于一些历史原因，Unix 出现过很多不同的分支，命令行的风格也因此有很多标准，比如：\nUnix 风格，选项采用单 - 加一个字母，比如 -v，短选项就是它，优点是足够简洁; BSD 风格，选项没有 -，没有任何的前缀，不知道有参数的情况怎么处理，没有研究; GNU 风格，采用 --，如 --version，长选项，扩展性好，但是要多打几个字母; 在网上找到一个搞笑漫画。\n查看系统进程有两种写法， ps aux（BSD 风格） 和 ps -elf（Unix 风格）。之前，我一直很郁闷为什么有这个区别。现在算是明白了。哈哈。\nPOSIX 的命令行风格算是取长补短的集合吧。什么是 POSIX 风格？可以查看这篇文档 命令参数语法。它同时提供了长短选项的标准。\n要明白的是，标准终究只是标准，很多命令其实并不遵循它。但自己在设计命令行规范的时候，最好还是要有一套标准，而参考最统一的标准肯定是没错的。\n总结 # 本文介绍了 Go 中 flag 包的使用，一般的场景已经足够使用了。\n博文地址：Go 命令行解析 flag 包之快速上手\n","date":"2019-11-23","externalUrl":null,"permalink":"/posts/2019-11-23-commandline-tool-flag-in-golang/","section":"文章","summary":"本篇文章是 Go 标准库 flag 包的快速上手篇。\n概述 # 开发一个命令行工具，视复杂程度，一般要选择一个合适的命令行解析库，简单的需求用 Go 标准库 flag 就够了，flag 的使用非常简单。\n","title":"Go 命令行解析 flag 包之快速上手","type":"posts"},{"content":"最近尝试在 B 站录些小视频，我的 B 站主页。录视频当是为了彻底搞懂某个知识点的最后一步吧，同时也希望能习得一些额外的能力。\n在讲 Go 如何实现 bitset 的时候，发现这块内容有点难讲。思考后，我决定通过文字辅以视频的方式说明，于是就写了这篇文章。\n相关代码已经放在了 github，地址如下：go-set-example\n如果发现有什么不妥的地方，欢迎大佬们指正，感谢。\nbitset 结构 # 之前我已经写过一篇题为 Go 中如何使用 Set 的文章，其中介绍了 bitset 一种最简单的应用场景，状态标志位，顺便还提了下 bitset 的实现思路。\n状态标志和一般的集合有什么区别呢？\n我的总结是主要一点，那就是状态标志中元素个数通常是固定的。而一般的集合中，元素个数通常是动态变化的。这会导致什么问题？\n一般，我们使用一个整数就足以表示状态标志中的所有状态，最大的 int64 类型，足足有 64 个二进制位，最多可以包含 64 个元素，完全足够使用。但如果是集合，元素数量和值通常都不固定。\n比如一个 bitset 集合最初可能只包含 1、2、4 几个元素，只要一个 int64 就能表示。如下：\n但如果再增加了一个元素，比如 64（一个 int64 的表示范围是 0-63），这已经超出了一个 int64 能表示的范围。该怎么办？\n一个 int64 无法表示，那就用多个呗。此时的结构如下：\n一个 int64 切片正好符合上面的结构。那我们就可以定义一个新的类型 BitSet，如下：\ntype BitSet struct { data []int64 size int } data 成员用于存放集合元素，切片的特点就是能动态扩容。\n还有，因为 bitset 中元素个数无法通过 len 函数获取，而具体的方法相对复杂一点，可增加一个 size 字段记录集合元素的个数。然后就可以增加一个 Size 方法。\nfunc (set *BitSet) Size() int { return set.size } 元素位置 # 定义好了 BitSet 类型，又产生了一个新的问题，如何定位存放元素的位置？在标志位的场景下，元素的值即是位置，所以这个问题不用考虑。但通用的集合不是如此。\n先看下 BitSet 的二进制位的分布情况。\n类似行列的效果，假设用 index 表示行（索引），pos 表示列（位置）。切片索引从 0 到 n，n 与集合中的最大元素有关。\n接下来确定 index 和 pos 的值。其实，之前的文章已经介绍过了。\nindex 可通过元素值整除字长，即 value / 64，转化为高效的位运算，即 value \u0026gt;\u0026gt; 6。\npos 可以通过元素值取模字长，即 value % 64，转化为高效的位运算，即 value \u0026amp; 0x3f，获取对应位置，然后用 1 \u0026lt;\u0026lt; uint(value % 0xf) 即可将位置转化为值。\n代码实现 # 理论再多，都不如 show me your code。开始编写代码吧！\n先定义一些常量。\nconst ( shift = 6 // 2^n = 64 的 n mask = 0x3f // n=6，即 2^n - 1 = 63，即 0x3f ) 就是前面提到的用于计算 index 和 pos 的两个常量。\n提供两个函数，用于方便 index 和 pos 上对应值的计算，代码如下：\nfunc index(n int) int { return n \u0026gt;\u0026gt; shift } // 相对于标志位使用场景中某个标志的值 func posVal(n int) uint64 { return 1 \u0026lt;\u0026lt; uint(n\u0026amp;mask) } 构造函数 # 提供了一个函数，用于创建初始 BitSet，且支持设置初始的元素。\n函数原型如下：\nfunc NewBitSet(ns ...int) *BitSet { // ... } 输出参数 ns 是一个 int 类型的变长参数，用于设置集合中的初始值。\n如果输入参数 ns 为空的话，new(BitSet) 返回空集合即可。\nif len(ns) == 0 { return new(BitSet) } 如果长度非空，则要计算要开辟的空间，通过计算最大元素的 index 可确定。\n// 计算多 bitset 开辟多个空间 max := ns[0] for _, n := range ns { if n \u0026gt; max { max = n } } // 如果 max \u0026lt; 0，直接返回空。 if max \u0026lt; 0 { return new(BitSet) } // 通过 max \u0026gt;\u0026gt; shift+1 计算最大值 max 所在 index // 而 index + 1 即为要开辟的空间 s := \u0026amp;BitSet{ data: make([]int64, index(max)+1), } 现在，可以向 BitSet 中添加元素了。\nfor _, n := range ns { if n \u0026gt;= 0 { // e \u0026gt;\u0026gt; shift 获取索引位置，即行，一般叫 index // e\u0026amp;mask 获取所在列，一般叫 pos，F1 0 F2 1 s.data[n\u0026gt;\u0026gt;shift] |= posVal(n) // 增加元素个数 s.size++ } } // 返回创建的 BitSet return s 元素已经全部添加完成！\nBitSet 的方法 # 接下来是重点了，为 BitSet 增加一些方法。主要是分成两类，一是常见的增删查等基础方法，二是集合的特有操作，交并差。\n基础方法 # 主要是几个方法，分别是 Add（增加）、Clear（清除） 、Contains（检查）以及返回元素个数。如果要有更好的性能和空间使用率，Add 和 Clear 还有考虑灵活的。\ncontains # 先讲 Contains，即检查是否存在某个元素。\n函数定义如下：\nfunc (set *BitSet) Contains(n int) bool { ... } 输入参数即是要检查的元素，输出是检查结果。\n实现代码如下：\n// 获取元素对应的 int64 的位置，如果超出 data 能表示的范围，直接返回。 i := index(n) if i \u0026gt;= len(set.data) { return false } return set.data[i]\u0026amp;posVal(n) != 0 核心就是 set.data[i]\u0026amp;posVal(n) != 0 这句代码，通过它判断是否存在指定元素。\nclear # 再谈 Clear，从集合中清除某个元素，\n函数定义如下：\nfunc (set *BitSet) Clear(n int) *BitSet { // ... } 实现代码如下：\n// 元素不能小于 0 if n \u0026lt; 0 { return set } // 计算切片索引位置，如果超出当前索引表示的范围，返回即可。 i := index(n) if i \u0026gt;= len(set.data) { return set } // 检查是否存在元素 if d[i]\u0026amp;posVal(n) != 0 { set.data[i] \u0026amp;^= posVal(n) set.size-- } 通过 \u0026amp;^ 实现指定位清除。同时要记得set.size-- 更新集合中元素的值。\n上面的实现中有个瑕疵，就是如果一些为被置零后，可能会出现高位全部为 0，此时应要通过 reslice 收缩 data 空间。\n具体怎么操作呢？\n通过对 set.data 执行检查，从高位检查首个不为 0 的 uint64，以此为基准进行 reslice。假设，这个方法名为 trim。\n实现代码如下：\nfunc (set *Set) trim() { d := set.data n := len(d) - 1 for n \u0026gt;= 0 \u0026amp;\u0026amp; d[n] == 0 { n-- } set.data = d[:n+1] } add # 接着，再说 Add 方法，向集合中添加某个元素。\n函数定义如下：\nfunc (set *BitSet) Add(n int) *BitSet { ... } 增加元素的话，先检查下是否有足够空间存放新元素。如果新元素的索引位置不在当前 data 表示的范围，则要进行扩容。\n实现如下：\n// 检测是否有足够的空间存放新元素 i := index(n) if i \u0026gt;= len(set.data) { // 扩容大小为 i+1 ndata := make([]uint64, i+1) copy(ndata, set.data) set.data = ndata } 一切准备就绪后，接下来就可以进行置位添加了。在添加前，先检测下集合是否已经包含了该元素。在添加完成后，还要记得要更新下 size。\n实现代码如下：\nif set.data[i]\u0026amp;posVal(n) == 0 { // 设置元素到集合中 set.data[i] |= posVal(n) s.size++ } 好了！基础的方法就介绍这么多吧。\n当然，这里的方法还可以增加更多，比如查找当前元素的下一个元素，将某个范围值都添加进集合等等等。\n集合方法 # 介绍完了基础的方法，再继续介绍集合一些特有的方法，交并差。\ncomputeSize # 在正式介绍这些方法前，先引入一个辅助方法，用于计算集合中的元素个数。之所以要引入这个方法，是因为交并差没有办法像之前在增删的时候更新 size，要重新计算一下。\n实现代码如下：\nfunc (set *BitSet) computeSize() int { d := set.data n := 0 for i, len := 0, len(d); i \u0026lt; len; i++ { if w := d[i]; w != 0 { n += bits.OnesCount64(w) } } return n } 这是一个不可导出的方法，只能内部使用。遍历 data 的每个 uint64，如果非 0，则统计其中的元素个数。元素个数统计用到了标准库中的 bits.OnesCount64 方法。\n方法定义 # 继续介绍集合的几个方法，它们的定义类似，都是一个 BitSet 与另一个 BitSet 的运算，如下：\n// 交集 func (set *BitSet) Intersect(other *BitSet) *BitSet { // ... } // 并集 func (set *BitSet) Union(other *BitSet) *BitSet { // ... } // 差集 func (set *BitSet) Difference(other *BitSet) *BitSet { // ... } intersect # 先介绍 Intersect，即计算交集的方法。\n一个重要前提，因为交集是 与运算，结果肯定位于两个参与运算的那个小范围集合中，所以，开辟空间和遍历可以缩小到这个范围进行。\n实现代码如下：\n// 首先，获取这个小范围的集合的长度 minLen := min(len(set.data), len(other.data)) // 以 minLen 开辟空间 intersectSet := \u0026amp;BitSet{ data: make([]uint64, minLen), } // 以 minLen 进行遍历计算交集 for i := minLen - 1; i \u0026gt;= 0; i-- { intersectSet.data[i] = set.data[i] \u0026amp; other.data[i] } intersectSet.size = set.computeSize() 这里通过遍历逐一对每个 uint64 执行 与运算 实现交集。在完成操作后，记得计算下 intersectSet 中元素个数，即 size 的值。\nunion # 再介绍并集 Union 方法。\n它的计算逻辑和 Intersect 相反。并集结果所占据的空间和以参与运算的两个集合的较大集合为准。\n实现代码如下：\nvar maxSet, minSet *BitSet if len(set.data) \u0026gt; len(other.data) { maxSet, minSet = set, other } else { maxSet, minSet = other, set } unionSet := \u0026amp;BitSet{ data: make([]uint64, len(maxSet.data)), } 创建的 unionSet 中，data 分配空间是 len(maxSet.data)。\n因为两个集合中的所有元素满足最终结果，但 maxSet 的高位部分无法通过遍历和 minSet 执行运算，直接拷贝进结果中即可。\nminLen := len(minSet.data) copy(unionSet.data[minLen:], maxSet.data[minLen:]) 最后，遍历两个集合 data，通过 或运算 计算剩余的部分。\nfor i := 0; i \u0026lt; minLen; i++ { unionSet.data[i] = set.data[i] | other.data[i] } // 更新计算 size unionSet.size = unionSet.computeSize() difference # 介绍最后一个与集合相关的方法，Difference，即差集操作。\n差集计算结果 differenceSet 的分配空间由被减集合 set 决定。其他的操作和 Intersect 和 Union 类似，位运算通过 \u0026amp;^ 实现。\nsetLen := len(set.data) differenceSet := \u0026amp;BitSet{ data: make([]uint64, setLen), } 如果 set 的长度大于 other，则需要先将无法进行差集运算的内容拷贝下。\nminLen := setLen if setLen \u0026gt; otherLen { copy(differenceSet.data[otherLen:], set.data[otherLen:]) minLen = otherLen } 记录下 minLen 用于接下来的位运算。\n// 遍历 data 执行位运算。 for i := 0; i \u0026lt; minLen; i++ { differenceSet.data[i] = set.data[i] \u0026amp;^ other.data[i] } differenceSet.size = differenceSet.computeSize() 遍历集合的元素 # 单独说下集合元素的遍历，之前查看集合元素一直都是通过 Contains 方法检查是否存在。能不能把集合中的每个元素全部遍历出来呢？\n再看下 bitset 的结构，如下：\n上面的集合中，第一行 int64 的第一个元素是 1，尾部有一位被置零。通过观察发现，前面有几个 0，第一个元素就是什么值。\n第二行 int64 的第一元素尾部没有 0，那它的值就是 0 吗？当然不是，还有前面一行的 64 位基础，所以它的值是 64+0。\n总结出什么规律了吗？笨，理论功底太差，满脑子明白，就是感觉写不清楚。看代码吧！\n先看函数定义：\nfunc (set *BitSet) Visit(do func(int) (skip bool)) (aborted bool) { //... } 输入参数是一个回调函数，通过它获取元素的值，不然每次都要写一大串循环运算逻辑，不太可能。回调函数的返回值 bool，表明是否继续遍历。Visit 的返回值表明是函数是非正常结束的。\n实现代码如下：\nd := set.data for i, len := 0, len(d); i \u0026lt; len; i++ { w := d[i] if w == 0 { continue } // 理论功力不好，不知道怎么描述了。哈哈 // 这小段代码可以理解为从元素值到 index 的逆运算， // 只不过得到的值是诸如 0、64、128 的第一个位置的值。 // 0 \u0026lt;\u0026lt; 6，还是 0，1 \u0026lt;\u0026lt; 6 就是 64，2 \u0026lt;\u0026lt; 6 的就是 128 n := i \u0026lt;\u0026lt; shift for w != 0 { // 000.....000100 64~128 的话，表示 66，即 64 + 2，这个 2 可以由结尾 0 的个数确定 // 那怎么获取结果 0 的个数呢？可以使用 bits.TrailingZeros64 函数 b := bits.TrailingZeros64(w) if do(n + b) { return true } // 将已经检查的位清零 // 为了保证尾部 0 的个数能代表元素的值 w \u0026amp;^= 1 \u0026lt;\u0026lt; uint64(b) } } 使用也非常方便，示例代码如下：\nset := NewBitSet(1, 2, 10, 99) set.Visit(func(n int) bool { fmt.Println(n) return false }) 好了，就说这么多吧！\n总结 # 本篇文章主要是参考了几个开源包的基础上，介绍了 bitset 的实现，比如 bit 和 bitset 等。总的来说，位运算就是没有那么直观，感觉脑子不够用了。\n","date":"2019-11-07","externalUrl":null,"permalink":"/posts/2019-11-07-bitset-in-golang/","section":"文章","summary":"最近尝试在 B 站录些小视频，我的 B 站主页。录视频当是为了彻底搞懂某个知识点的最后一步吧，同时也希望能习得一些额外的能力。\n在讲 Go 如何实现 bitset 的时候，发现这块内容有点难讲。思考后，我决定通过文字辅以视频的方式说明，于是就写了这篇文章。\n","title":"详细介绍 Go 中如何实现 bitset","type":"posts"},{"content":"为什么不再需要设置 GOROOT 呢？推荐读两篇英文文章，我意译了下，将它们放在了一篇里。\n第一篇是关于 Go 1.10 之前，怎么设置 GOROOT，发表与 2013 年。第二篇 是从 Go 1.10 开始，如何处理 GOROOT，时间是 2018 年，Go 源码提交日志。这篇非常短小。\n读完后，你会发现，大多数情况下，你都不用手动设置 GOROOT 了。\n第一篇 # 作者：Dave Cheney | 地址：you-dont-need-to-set-goroot-really\n一篇小短文，解释了为什么在编译和使用 Go 时，不需要设置 GOROOT。\n概要性介绍 # 一般来说，在 Go 1.0 之后，编译和使用 GO 不再需要设置 GOROOT。事实上，如果你的电脑上存在多个版本的 Go 语言环境，设置 GOROOT 可能产生一些问题。\nGOPATH 仍然需要设置。\n从 Go 1.0 开始，GOPATH 就被强烈推荐。随着 Go 1.1 的发布，GOPATH 已经是强制性的了。\n为什么不再要设置 GOROOT？ # 谈些 Go 环境变量的历史吧！\nGo 的资深老前辈们可能还记得，曾经的 Go 不仅要设置 GOROOT，还需要设置 GOOS 和 GOARCH。之所以要设置 GOROOT，是因为 Make 在编译构建的时候，引入了 GOROOT 中的内容，要提前设置 GOROOT 作为了它们的基本路径。\n随着 go tool 的引入，Go 1.0 之前，GOOS 和 GOARCH 已经变成可选了，因为构建脚本已经能自动检测出系统类别和 CPU 架构。在 Go 1.0 的发布后，引入了 cmd/dist 引导构建工具，GOOS 和 GOARCH 真正意义上是可选项了，仅仅在交叉编译时才会用到。\n不需要设置 GOOS 和 GOARCH，那 GOROOT 呢？\nGOROOT 定义为指定安装 GO 的根目录。在之前的 Makefile 中，引入其他 Makefile 时，将它作为基础路径。而且，Go 1.0 之后，go tool 利用它查找 Go 编译器（保存在 $GOROOT/pkg/tool/$GOOS_$GOARCH）和标准库（在 $GOROOT/pkg/$GOOS_$GOARCH）。如果你是一名 Java 开发者，可以将 GOROOT 理解为 JAVA_HOME。\n源码编译 Go，GOROOT 将自动发现（all.bash 的上级目录），然后设置到 go 工具链。\n如下命令查看：\n$ echo $GOROOT $ go env /home/dfc/go 从 golang.org 下载的二进制包或者系统方式安装的 Go 环境，也已在工具链中设置了正确的 GOROOT。\n一个例子，比如 Ubutun 12.04 下，安装了 Go 1.0。\n$ dpkg -l golang-{go,src} | grep ^ii $ go /usr/bin/go $ go env GOROOT /usr/lib/go 我们可以看出，Go 工具链被安装在了 /usr/bin/go 下，GOROOT 内置为 /usr/lib/go\n为什么不应该设置 GOROOT\n我们不应该设置 GOROOT，是因为 Go 工具链已经内置了正确的值。\n设置 GOROOT 将会覆盖掉保存在 go 工具链中的默认值，可能会导致 go 执行不同版本的编译器和标准库文件。\n两种情况下，你需要设置 GOROOT。在官方的 安装介绍 有相关的描述。\n如果你是 Linux、FreeBSD 或者 OS X 用户，下载了 zip 和 tarball 的二进制包安装环境。这些二进制的默认环境位于 /usr/local/go，建议你将 Go 安装到这个位置。如果选择不这么做，就必须设置到你指定的目录下。 如果你是 Windows 用户，使用 zip 二进制包安装，默认的 GOROOT 在 C:\\Go 目录下。如果你将 Go 安装在其他位置，请设置 GOROOT 到指定的目录。 其他细节 # 本文已经介绍了当通过源码编译 Go 环境的时候，GOROOT 如何自动发现的。但如果 GOROOT 与 all.bash 所在位置并不匹配呢？比如，在临时目录下编译 Go 环境，如何正确地设置 GOROOT 呢？答案是使用 GOROOT_FINAL，它将被用于覆盖自动发现的 GOROOT，设置到 GO 工具链中。\n举个例子，在 Debian/Ubuntu 上，构建程序会将 GOROOT_FINAL 的值设置为 /usr/lib/go。保持 GOROOT 是未设置状态，使构建编译愉快地执行。构建完成后，将 Go 工具链安装到 /usr/bin 目录下，编译器、源码和包安装到 /usr/lib/go 下。\n注意点 # 如果使用二进制包安装 Go 环境，有些特殊情况需要处理，本文已经作了相关描述。\n虽然构建系统能自动检测，但如果 all.bash 的父级目录不满足 GOROOT 要求，也需要另外处理。\n第二篇 # 翻译自 Go 的提交日志，地址：use os.Executable to find GOROOT。\nGo 1.10 开始，通过 use os.Executable 查找 GOROOT。\n之前，我们是通过 make.sh 编译构建 GOROOT，但如果将整个目录移动到新的路径下，这会使 Go 工具链无法正常工作。\n如何解决这个问题呢？\n一是可以将源码重新编译，但如果新位置在其他用户的目录下，可能就无法这么操作了。\n二是，通过设置 GOROOT 环境变量的方式解决，但另外设置 GOROOT 是不推荐的，因为它可能使一个环境下 go tool 使用了另一个环境下 compile。\n这次的修改，go tool 将通过相对路径的方式确定 GOROOT，通过使用 os.Execute 函数。同时，还会检查 $GOROOT/pkg/tool 目录是否存在，以避免下面的两种情况。\n$ ln -s $GOROOT/bin/go /usr/local/bin/go 和\n$ PATH=$HOME/bin:$PATH $ GOPATH=$HOME $ ln -s $GOROOT/bin/go $HOME/bin/go 另外，如果当前的执行路径并不在 GOROOT 下，将会通过软连接找到真正的命令的位置，检查这个路径是否是 GOROOT。\n","date":"2019-11-06","externalUrl":null,"permalink":"/posts/2019-11-06-dont-set-goroot/","section":"文章","summary":"为什么不再需要设置 GOROOT 呢？推荐读两篇英文文章，我意译了下，将它们放在了一篇里。\n第一篇是关于 Go 1.10 之前，怎么设置 GOROOT，发表与 2013 年。第二篇 是从 Go 1.10 开始，如何处理 GOROOT，时间是 2018 年，Go 源码提交日志。这篇非常短小。\n","title":"你真的不用再设置 GOROOT 了","type":"posts"},{"content":"之前的 Go 笔记系列，已经完成到了开发环境搭建，原本接下来的计划就是到语法部分了，但后来一直没有前进。主要是因为当时的工作比较忙，分散了精力，于是就暂时放下了。\n最近，准备重新把之前计划捡起来。\n第一步，肯定是了解 Go 基础语法部分。原本计划是写 Go 编码的一些基础知识，但纯粹聊什么是关键字、标识符、字面量、操作符实在有点无聊。\n突然想到，词法分析这块知识还没仔细研究过，那就从这个角度出发吧。通过逐步地拆解，将各个 token 进行归类。\n概述 # 我们知道，编译型语言（比如 Go）的源码要经过编译和链接才能转化为计算机可以执行的程序，这个过程的第一步就是词法分析。\n什么是词法分析呢？\n它就是将源代码转化为一个个预先定义的 token 的过程。为了便于理解，我们将其分为两个阶段进行介绍。\n第一阶段，对源码串进行扫描，按预先定义的 token 规则进行匹配并切分为一个个有语法含义、最小单元的字符串，即词素（lexme），并在此基础上将其划归为某一类 token。这个阶段，一些字符可能会被过滤掉，比如，空白符、注释等。\n第二阶段，通过评估器 Evaluator 评估扫描出来的词素，并确定它字面值，生成最终的 Token。\n是不是有点不好理解呢？\n如果之前从未接触过这块内容，可能没有直观感受。其实，看着很复杂，但的确非常简单。\n一个简单的示例 # 先看一段代码，经典的 hello world，如下：\npackage main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello World\u0026#34;) } 我们可以通过这个例子的源码逐步拆解词法分析的整个流程。\n什么是词素 # 理论性的概念就不说了，直接看效果吧。\n首先，将这段示例代码通过词法分析的第一阶段，我们将会得到如下内容：\npackage main \\n import \u0026#34;fmt\u0026#34; \\n func main ( ) { \\n fmt . Println ( \u0026#34;Hello World\u0026#34; ) \\n } 输出的这一个个独立的字符序列就是词素。\n词素的切分规划和语言的语法规则有关。此处的输出中除了一些可见的字符，换行符同样也具有语法含义，因为 Go 不像 C/C++ 必须是分号分隔语句，也可以通过换行符分隔。\n源码分割为一个个词素的过程是有一定的规则的，这和具体的语言有关。但虽有差异，其实规则都差不多，无非两种，一是通过无语法含义的字符（空格符、制表符等）切分，还有是每个词素可以用作为分隔符。\n什么是 token # token，也称为词法单元、记号等，它由名称和字面值两部分组成。从词素到 token 有固定的对应关系，而且并非所有的 token 都有字面值。\n将 hello world 的源码转化为 token，我们将会得到如下的一张对应表格。\nlexme name value package PACKAGE \u0026ldquo;package\u0026rdquo; main IDENT \u0026ldquo;main\u0026rdquo; \\n SEMICOLON \u0026ldquo;\\n\u0026rdquo; import IMPORT \u0026ldquo;import\u0026rdquo; \u0026ldquo;fmt\u0026rdquo; STRING \u0026ldquo;\\\u0026ldquo;fmt\\\u0026rdquo;\u0026rdquo; \\n SEMICOLON \u0026ldquo;\\n\u0026rdquo; func FUNC \u0026ldquo;func\u0026rdquo; main IDENT \u0026ldquo;main\u0026rdquo; ( LPAREN \u0026quot;\u0026quot; ) RPAREN \u0026quot;\u0026quot; { LBRACE \u0026quot;\u0026quot; fmt IDENT \u0026ldquo;fmt\u0026rdquo; . PERIOD \u0026quot;\u0026quot; Println IDENT \u0026ldquo;Println\u0026rdquo; ( LPAREN \u0026quot;\u0026quot; \u0026ldquo;Hello World\u0026rdquo; STRING \u0026ldquo;\u0026quot;Hello World\u0026quot;\u0026rdquo; ) RPAREN \u0026quot;\u0026quot; \\n SEMICOLON \u0026ldquo;\\n\u0026rdquo; } LBRACE \u0026quot;\u0026quot; \\n SEMICOLON \u0026ldquo;\\n\u0026rdquo; 稍微有点长，因为这里没有省略。表格中的第一列是原始内容，第二列对应的 token 的名称，最后一列是 token 的字面值。\n从表格中可以观察出，其中有一些 token 并没有值，比如，括号、点，名称本身已经表示了它们的内容。\ntoken 的分类 # token 一般可以分为关键字、标识符、字面量、操作符这四个大类。这个分类其实在 Go 的源码中有非常明显的体现。\n查看源码文件 src/go/token/token.go，将会找到 Token 类型如下的几个方法。\n// 是否是字面常量 func (tok Token) IsLiteral() bool { return literal_beg \u0026lt; tok \u0026amp;\u0026amp; tok \u0026lt; literal_end } // 是否是操作符 func (tok Token) IsOperator() bool { return operator_beg \u0026lt; tok \u0026amp;\u0026amp; tok \u0026lt; operator_end } // 是否是关键字 func (tok Token) IsKeyword() bool { return keyword_beg \u0026lt; tok \u0026amp;\u0026amp; tok \u0026lt; keyword_end } 代码非常简单，通过比较确定 Token 是否位于指定范围确定它的类型。上面的这三个方法分别对应于判断 Token 是字面常量、操作符还是关键字。\n额？怎么没有标识符呢？\n当然也有啦，只不过它不是 Token 的方法，而是单独的一个函数。如下：\nfunc IsIdentifier(name string) bool { for i, c := range name { if !unicode.IsLetter(c) \u0026amp;\u0026amp; c != \u0026#39;_\u0026#39; \u0026amp;\u0026amp; (i == 0 || !unicode.IsDigit(c)) { return false } } return name != \u0026#34;\u0026#34; \u0026amp;\u0026amp; !IsKeyword(name) } 我们常说的变量、常量、函数、方法的名称不能为关键字，且必须是由字母、下划线或数字组成，且名称的开头不能为数字的规则，看到这个函数是不是一些就明白了。\n到这里，其实已经写的差不多了。但想想还是拿其中一个类型再简单说说吧。\n关键字 # 就以关键字为例吧，Go 中的关键字有哪些呢？\n继续看源码。将之前那段如何判断一个 token 是关键字的代码再看一遍。如下：\nfunc (tok Token) IsKeyword() bool { return keyword_beg \u0026lt; tok \u0026amp;\u0026amp; tok \u0026lt; keyword_end } 只要 Token 大于 keyword_beg 且小于 keyword_end 即为关键字，看起来还挺好理解的。那在 keyword_beg 和 keyword_end 之间有哪些关键字呢？代码如下：\nconst ( ... keyword_beg // Keywords BREAK CASE CHAN CONST CONTINUE ... SELECT STRUCT SWITCH TYPE VAR keyword_end ... ) 总共梳理出了 25 个关键字。如下：\nbreak case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var 关键字的确挺少的。可见。。。\n嗯？！\n是不是猜到我要说，Go 语言就是简洁，关键字的都这么少。你看 Java，足足有 53 个关键字，其中有两个是保留字。你再看看 Go，连保留字都没有，就是这么自信。\n既然你猜到了，那我还是先不说了吧。\n其他 # 操作符和字面常量就不追了，思路都是一样的。\nGo 中的操作符有 47 个，比如赋值运算符、位运算符、算术运算符，比较运算符，还有其他的操作符。相信我吧，都是从源码中数出来的，没有看任何资料。[此处应该放个捂脸笑]。\n字面常量呢？\n有 5 种类型，分别是 INT（整型）、FLOAT（浮点型）、IMG（复数类型）、CHAR（字符型）、STRING（字符串型）。\n总结 # 文章写完了，前面扯了那么一堆废话，其实就只是为了介绍 Go 语法中用到的关键字、标识符、运算符、字面量从哪里找。并且，最终它们如何使用也没有怎么说明。\n博文地址：从词法分析角度看 Go 代码组成\n阅读资料 # Go 程序是怎么跑起来的 go-lexer 词法分析 Lexical analysis 词法分析 ","date":"2019-11-03","externalUrl":null,"permalink":"/posts/2019-11-03-golang-lexical-analysis/","section":"文章","summary":"之前的 Go 笔记系列，已经完成到了开发环境搭建，原本接下来的计划就是到语法部分了，但后来一直没有前进。主要是因为当时的工作比较忙，分散了精力，于是就暂时放下了。\n","title":"从词法分析角度看 Go 代码的组成","type":"posts"},{"content":"最近，我开发了一个非常简单的小工具，总的代码量 200 行不到。今天，简单介绍下它。这是个什么工具呢？它是一个用于可视化展示 Go Module 依赖关系的工具。\n为何开发 # 为什么会想到开发这个工具？主要有两点原因：\n一是最近经常看到大家在社区讨论 Go Module。于是，我也花了一些时间研究了下。期间，遇到了一个需求，如何清晰地识别模块中依赖项之间的关系。一番了解后，发现了 go mod graph。\n效果如下：\n$ go mod graph github.com/poloxue/testmod golang.org/x/text@v0.3.2 github.com/poloxue/testmod rsc.io/quote/v3@v3.1.0 github.com/poloxue/testmod rsc.io/sampler@v1.3.1 golang.org/x/text@v0.3.2 golang.org/x/tools@v0.0.0-20180917221912-90fa682c2a6e rsc.io/quote/v3@v3.1.0 rsc.io/sampler@v1.3.0 rsc.io/sampler@v1.3.1 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c rsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c 每一行的格式是 模块 依赖模块，基本能满足要求，但总觉得还是不那么直观。\n二是我之前手里有一个项目，包管理一直用的是 dep。于是，我也了解了下它，把官方文档仔细读了一遍。其中的某个章节介绍了依赖项可视化展示的方法。\n文档中给出的包关系图：\n看到这张图的时候，眼睛瞬间就亮了，图形化就是优秀，不同依赖之间的关系一目了然。这不就是我想要的效果吗？666，点个赞。\n但 \u0026hellip;，随之而来的问题是，go mod 没这个能力啊。怎么办？\n如何实现 # 先看看是不是已经有人做了这件事了。网上搜了下，没找到。那是不是能自己实现？应该可以借鉴下 dep 的思路吧？\n如下是 dep 依赖实现可视化的方式：\n# linux $ sudo apt-get install graphviz $ dep status -dot | dot -T png | display # macOS $ brew install graphviz $ dep status -dot | dot -T png | open -f -a /Applications/Preview.app # Windows \u0026gt; choco install graphviz.portable \u0026gt; dep status -dot | dot -T png -o status.png; start status.png 这里展示了三大系统下的使用方式，它们都安装了一个软件包，graphviz。从名字上看，这应该是一个用来实现可视化的软件，即用来画图的。事实也是这样，可以看看它的官网。\n再看下它的使用，发现都是通过管道命令组合的方式，而且前面的部分基本相同，都是 dep status -dot | dot -T png。后面的部分在不同的系统就不同了，Linux 是 display，MacOS 是 open -f -a /Applications/Preview.app，Window 是 start status.png。\n稍微分析下就会明白，前面是生成图片，后面是显示图片。因为不同系统的图片展示命令不同，所以后面的部分也就不同了。\n现在关心的重点在前面，即 dep status -dot | dot -T png 干了啥，它究竟是如何实现绘图的？大致猜测，dot -T png 是由 dep status -dot 提供的数据生成图片。继续看下 dep status -dot 的执行效果吧。\n$ dep status -dot digraph { node [shape=box]; 2609291568 [label=\u0026#34;github.com/poloxue/hellodep\u0026#34;]; 953278068 [label=\u0026#34;rsc.io/quote\\nv3.1.0\u0026#34;]; 3852693168 [label=\u0026#34;rsc.io/sampler\\nv1.0.0\u0026#34;]; 2609291568 -\u0026gt; 953278068; 953278068 -\u0026gt; 3852693168; } 咋一看，输出的是一段看起来不知道是啥的代码，这应该是 graphviz 用于绘制图表的语言。那是不是还有学习下？当然不用啊，这里用的很简单，直接套用就行了。\n试着分析一下吧，前面两行可以不用关心，这应该是 graphviz 特定的写法，表示要画的是什么图。我们主要关心如何将数据以正确形式提供出来。\n2609291568 [label=\u0026#34;github.com/poloxue/hellodep\u0026#34;]; 953278068 [label=\u0026#34;rsc.io/quote\\nv3.1.0\u0026#34;]; 3852693168 [label=\u0026#34;rsc.io/sampler\\nv1.0.0\u0026#34;]; 2609291568 -\u0026gt; 953278068; 953278068 -\u0026gt; 3852693168; 一看就知道，这里有两种结构，分别是为依赖项关联 ID ，和通过 ID 和 -\u0026gt; 表示依赖间的关系。\n按上面的猜想，我们可以试着画出一个简单的图, 用于表示 a 模块依赖 b 模块。执行命令如下，将绘图代码通过 each 管道的方式发送给 dot 命令。\n$ echo \u0026#39;digraph { node [shape=box]; 1 [label=\u0026#34;a\u0026#34;]; 2 [label=\u0026#34;b\u0026#34;]; 1 -\u0026gt; 2; }\u0026#39; | dot -T png | open -f -a /Applications/Preview.app 效果如下：\n绘制一个依赖关系图竟然这么简单。\n看到这里，是不是发现问题已经变得非常简单了。我们只要将 go mod graph 的输出转化为类似的结构就能实现可视化了。\n开发流程介绍 # 接下来，开发这个小程序吧，我将这个小程序命名为 modv，即 module visible 的意思。项目源码位于 poloxue/modv。\n接收管道的输入 # 先要检查数据输入管道是否正常。\n我们的目标是使用类似 dep 中作图的方式，go mod graph 通过管道将数据传递给 modv。因此，要先检查 os.Stdin，即检查标准输入状态是否正常， 以及是否是管道传输。\n下面是 main 函数的代码，位于 main.go 中。\nfunc main() { info, err := os.Stdin.Stat() if err != nil { fmt.Println(\u0026#34;os.Stdin.Stat:\u0026#34;, err) PrintUsage() os.Exit(1) } // 是否是管道传输 if info.Mode()\u0026amp;os.ModeNamedPipe == 0 { fmt.Println(\u0026#34;command err: command is intended to work with pipes.\u0026#34;) PrintUsage() os.Exit(1) } 一旦确认输入设备一切正常，我们就可以进入到数据读取、解析与渲染的流程了。\nmg := NewModuleGraph(os.Stdin) mg.Parse() mg.Render(os.Stdout) } 接下来，开始具体看看如何实现数据的处理流程。\n抽象实现结构 # 先定义一个结构体，并大致定义整个流程。\ntype ModGraph struct { Reader io.Reader // 读取数据流 } func NewModGraph(r io.Reader) *ModGraph { return \u0026amp;ModGraph{Reader: r} } // 执行数据的处理转化 func (m *ModGraph) Parse() error {} // 结果渲染与输出 func (m *ModGraph) Render(w io.Writer) error {} 再看下 go mod graph 的输出吧，如下：\ngithub.com/poloxue/testmod golang.org/x/text@v0.3.2 github.com/poloxue/testmod rsc.io/quote/v3@v3.1.0 ... 每一行的结构是 模块 依赖项。现在的目标是要它解析成下面这样的结构：\ndigraph { node [shape=box]; 1 github.com/poloxue/testmod; 2 golang.org/x/text@v0.3.2; 3 rsc.io/quote/v3@v3.1.0; 1 -\u0026gt; 2; 1 -\u0026gt; 3; } 前面说过，这里包含了两种不同的结构，分别是模块与 ID 关联关系，以及模块 ID 表示模块间的依赖关联。为 ModGraph 结构体增加两个成员表示它们。\ntype ModGraph struct { r io.Reader // 数据流读取实例，这里即 os.Stdin // 每一项名称与 ID 的映射 Mods map[string]int // ID 和依赖 ID 关系映射，一个 ID 可能依赖多个项 Dependencies map[int][]int } 要注意的是，增加了两个 map 成员后，记住要在 NewModGraph 中初始化下它们。\nmod graph 输出解析 # 如何进行解析？\n介绍到这里，目标已经很明白了。就是要将输入数据解析到 Mods 和 Dependencies 两个成员中，实现代码都在 Parse 方法中。\n为了方便进行数据读取，首先，我们利用 bufio 基于 reader 创建一个新的 bufReader，\nfunc (m *ModGraph) Parse() error { bufReader := bufio.NewReader(m.Reader) ... 为便于按行解析数据，我们通过 bufReader 的 ReadBytes() 方法循环一行一行地读取 os.Stdin 中的数据。然后，对每一行数据按空格切分，获取到依赖关系的两项。代码如下：\nfor { relationBytes, err := bufReader.ReadBytes(\u0026#39;\\n\u0026#39;) if err != nil { if err == io.EOF { return nil } return err } relation := bytes.Split(relationBytes, []byte(\u0026#34; \u0026#34;)) // module and dependency mod, depMod := strings.TrimSpace(string(relation[0])), strings.TrimSpace(string(relation[1])) ... } 接下来，就是将解析出来的依赖关系组织到 Mods 和 Dependencies 两个成员中。模块 ID 是生成规则采用的是最简单的实现方式，从 1 自增。实现代码如下：\nmodId, ok := m.Mods[mod] if !ok { modId = serialID m.Mods[mod] = modId serialID += 1 } depModId, ok := m.Mods[depMod] if !ok { depModId = serialID m.Mods[depMod] = depModId serialID += 1 } if _, ok := m.Dependencies[modId]; ok { m.Dependencies[modId] = append(m.Dependencies[modId], depModId) } else { m.Dependencies[modId] = []int{depModId} } 解析的工作到这里就结束了。\n渲染解析的结果 # 这个小工具还剩下最后一步工作要做，即将解析出来的数据渲染出来，以满足 graphviz 工具的作图要求。实现代码是 Render部分：\n首先，定义一个模板，以生成满足要求的输出格式。\nvar graphTemplate = `digraph { node [shape=box]; {{ range $mod, $modId := .mods -}} {{ $modId }} [label=\u0026#34;{{ $mod }}\u0026#34;]; {{ end -}} {{- range $modId, $depModIds := .dependencies -}} {{- range $_, $depModId := $depModIds -}} {{ $modId }} -\u0026gt; {{ $depModId }}; {{ end -}} {{- end -}} } ` 这一块没啥好介绍的，主要是要熟悉 Go 中的 text/template 模板的语法规范。为了展示友好，这里通过 - 实现换行的去除，整体而言不影响阅读。\n接下来，看 Render 方法的实现，把前面解析出来的 Mods 和 Dependencies 放入模板进行渲染。\nfunc (m *ModuleGraph) Render(w io.Writer) error { templ, err := template.New(\u0026#34;graph\u0026#34;).Parse(graphTemplate) if err != nil { return fmt.Errorf(\u0026#34;templ.Parse: %v\u0026#34;, err) } if err := templ.Execute(w, map[string]interface{}{ \u0026#34;mods\u0026#34;: m.Mods, \u0026#34;dependencies\u0026#34;: m.Dependencies, }); err != nil { return fmt.Errorf(\u0026#34;templ.Execute: %v\u0026#34;, err) } return nil } 现在，全部工作都完成了。最后，将这个流程整合到 main 函数。接下来就是使用了。\n使用体验 # 开始体验下吧。补充一句，这个工具，我现在只测试了 Mac 下的使用，如有问题，欢迎提出来。\n首先，要先安装一下 graphviz，安装的方式在本文开头已经介绍了，选择你的系统安装方式。\n接着是安装 modv，命令如下：\n$ go get github.com/poloxue/modv 安装完成！简单测试下它的使用。\n以 MacOS 为例。先下载测试库，github.com/poloxue/testmod。 进入 testmod 目录执行命令：\n$ go mod graph | modv | dot -T png | open -f -a /Applications/Preview.app 如果执行成功，将看到如下的效果：\n完美地展示了各个模块之间的依赖关系。\n一些思考 # 本文是篇实践性的文章，从一个简单想法到成功呈现出一个可以使用的工具。虽然，开发起来并不难，从开发到完成，仅仅花了一两个小时。但我的感觉，这确实是个有实际价值的工具。\n还有一些想法没有实现和验证，比如一旦项目较大，是否可以方便的展示某个指定节点的依赖树，而非整个项目。还有，在其他项目向 Go Module 迁移的时候，这个小工具是否能产生一些价值。\n","date":"2019-10-23","externalUrl":null,"permalink":"/posts/2019-10-23-golang-module-visualization/","section":"文章","summary":"最近，我开发了一个非常简单的小工具，总的代码量 200 行不到。今天，简单介绍下它。这是个什么工具呢？它是一个用于可视化展示 Go Module 依赖关系的工具。\n为何开发 # 为什么会想到开发这个工具？主要有两点原因：\n","title":"Go Module 依赖关系的可视化","type":"posts"},{"content":"本文主要介绍的是关于 Go 如何解析 json 内部结构不确定的情况。\n首先，我们直接看一个来提问吧。\n问题如下：\n上游传递不确定的json，如何透传给下游业务？比如，我解析参数\n{ \u0026#34;test\u0026#34;: 1, \u0026#34;key\u0026#34;: { \u0026#34;k1\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;k2\u0026#34;: 2 } } 但是key 结构体下面是未知的。可能是K1 K2 K3 \u0026hellip; KN。如何解析传递那？\n对于 json 格式数据的解析，如果其中的某个成员结构不确定。\n我总结一般有几种方式处理。\n常见的几种方案 # 第一个方案，也是最容易想到的，将那个不确定的成员用 map[string]interface{} 替代。\ntype Data struct { Test int `json:\u0026#34;test\u0026#34;` Key map[string]interface{} `json:\u0026#34;test\u0026#34;` } 但问题是，这种方式太坑，每次从 key 中拿数据，都要做类型检查，判断是否 ok。\n第二种，既然 map[string]interface{} 的方式太坑，那如果要是能用结构体就好了。\n虽然其中某个成员的结构不确定，但如果共性字段比较多，如都是与人相关，那肯定都有名字，年龄之类的字段，但如果是教师和学生，就会有一些不同的字段，把所有的不同字段都包含进来即可。但如果不同字段太多，那也不是很方便。\n第三种，终极解决方案，如果能先解析第一层的结构，再根据第一层的结果，确定第二层的结构，那就方便多了。不确定的成员依然用 map[string]interface{} 表示，确定结构后，再将 map[string]interface{} 解析为具体的某个结构。结构体使用起来就方便很多了。\n问题最终就变成了如何将 map[string]interface{} 转化为 struct，这个过程必然会用到反射，可以自己实现。但其他人早造就想到了，一个第三方库，地址：https://github.com/mitchellh/mapstructure 。\n一个实际的案例 # 看一个我工作遇到的一个实际案例。\n我在工作中，数据库数据实时更新到 elasticsearch，在实践过程中遇到了一些 JSON 数据处理的问题。\n什么样的数据呢？\n实时数据获取是通过 binlog 解析推送而来的的数据，并通过消息队列 kafka 传输给处理程序。\n收到的 JSON，类似如下形式。\n{ \u0026#34;type\u0026#34;: \u0026#34;UPDATE\u0026#34;, \u0026#34;database\u0026#34;: \u0026#34;blog\u0026#34;, \u0026#34;table\u0026#34;: \u0026#34;blog\u0026#34;, \u0026#34;data\u0026#34;: [ { \u0026#34;blogId\u0026#34;: \u0026#34;100001\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;title\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;this is a blog\u0026#34;, \u0026#34;uid\u0026#34;: \u0026#34;1000012\u0026#34;, \u0026#34;state\u0026#34;: \u0026#34;1\u0026#34; } ] } 简单说下数据的逻辑，type 表示数据库事件是新增、更新还是删除事件，database 表示对应的数据库名称，table 表示相应的表名称，data 即为数据库中数据。\n怎么处理这串 JSON 呢？\njson 转化为 map # 最先想到的方式就是通过 json.Unmarshal 将 JSON 转化 map[string]interface{}。\n示例代码：\nfunc main () { msg := []byte(`{ \u0026#34;type\u0026#34;: \u0026#34;UPDATE\u0026#34;, \u0026#34;database\u0026#34;: \u0026#34;blog\u0026#34;, \u0026#34;table\u0026#34;: \u0026#34;blog\u0026#34;, \u0026#34;data\u0026#34;: [ { \u0026#34;blogId\u0026#34;: \u0026#34;100001\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;title\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;this is a blog\u0026#34;, \u0026#34;uid\u0026#34;: \u0026#34;1000012\u0026#34;, \u0026#34;state\u0026#34;: \u0026#34;1\u0026#34; } ]}`) var event map[string]interface{} if err := json.Unmarshal(msg, \u0026amp;event); err != nil { panic(err) } fmt.Println(event) } 打印结果如下：\nmap[data:[map[title:title content:this is a blog uid:1000012 state:1 blogId:100001]] type:UPDATE database:blog table:blog] 到此，就成功解析出了数据。接下来是使用它，但我觉得 map 通常有几个不足。\n通过 key 获取数据，可能出现不存在的 key，为了严谨，需要检查 key 是否存在； 相对于结构体的方式，map数据提取不便且不能利用 IDE 补全检查，key 容易写错； 针对这个情况，可以怎么处理呢？如果能把 JSON 转化为struct 就好了。\njson 转化为 struct # 在 GO 中，json 转化为 struct 也非常方便，只需提前定义好转化的 struct 即可。我们先来定义一下转化的 struct。\ntype Event struct { Type string `json:\u0026#34;type\u0026#34;` Database string `json:\u0026#34;database\u0026#34;` Table string `json:\u0026#34;table\u0026#34;` Data []map[string]string `json:\u0026#34;data\u0026#34;` } 说明几点\n实际场景中，canal 消息的 data 结构是由表决定的，在 JSON 成功解析前无法提前知道，所以这里定义为 map[string]string； 转化的结构体成员必须是可导出的，所以成员变量名都是大写，而与 JSON 的映射通过 json:\u0026quot;tagName\u0026quot; 的 tagName 完成。 解析代码非常简单，如下：\ne := Event{} if err := json.Unmarshal(msg, \u0026amp;e); err != nil { panic(err) } fmt.Println(e) 打印结果：\n{UPDATE blog blog [map[blogId:100001 title:title content:this is a blog uid:1000012 state:1]]} 接下来，数据解雇就方便了不少，比如事件类型获取，通过 event.Type 即可完成。不过，要泼盆冷水，因为 data 还是 []map[string]string 类型，依然有 map 的那些问题。\n能不能把 map 转化为 struct ?\nmap 转化为 struct # 据我所知，map 转为转化为 struct，GO 是没有内置的。如果要实现，需要依赖于 GO 的反射机制。\n不过，幸运的是，其实已经有人做了这件事，包名称为 mapstructure，使用也非常简单，敲一遍它提供的几个例子就学会了。README 中也说了，该库主要是遇到必须读取一部分 JSON 才能知道剩余数据结构的场景，和我的场景如此契合。\n安装命令如下：\n$ go get https://github.com/mitchellh/mapstructure 开始使用前，先定义 map 将转化的 struct 结构，即 blog 结构体，如下：\ntype Blog struct { BlogId string `mapstructure:\u0026#34;blogId\u0026#34;` Title string `mapstructrue:\u0026#34;title\u0026#34;` Content string `mapstructure:\u0026#34;content\u0026#34;` Uid string `mapstructure:\u0026#34;uid\u0026#34;` State string `mapstructure:\u0026#34;state\u0026#34;` } 因为，接下来要用的是 mapstructure 包，所以 struct tag 标识不再是 json，而是 mapstructure。\n示例代码如下：\ne := Event{} if err := json.Unmarshal(msg, \u0026amp;e); err != nil { panic(err) } if e.Table == \u0026#34;blog\u0026#34; { var blogs []Blog if err := mapstructure.Decode(e.Data, \u0026amp;blogs); err != nil { panic(err) } fmt.Println(blogs) } event 的解析和前面的一样，通过 e.Table 判断是是否来自 blog 表的数据，如果是，使用 Blog 结构体解析。接下来通过 mapstructure 的 Decode 完成解析。\n打印结果如下：\n[{100001 title this is a blog 1000012 1}] 到此，似乎已经完成了所有工作。非也！\n弱类型解析 # 不知道大家有没有发现一个问题，那就是 Blog 结构体中的所有成员都是 string，这应该是 canal 做的事情，所有的值类型都是 string。但实际上 blog 表中的 uid 和 state 字段其实都是 int。\n理想的结构体定义应该是下面这样。\ntype Blog struct { BlogId string `mapstructure:\u0026#34;blogId\u0026#34;` Title string `mapstructrue:\u0026#34;title\u0026#34;` Content string `mapstructure:\u0026#34;content\u0026#34;` Uid int32 `mapstructure:\u0026#34;uid\u0026#34;` State int32 `mapstructure:\u0026#34;state\u0026#34;` } 但是当把新的 Blog 类型代入之前的代码，会如下的错误。\npanic: 2 error(s) decoding: * \u0026#39;[0].state\u0026#39; expected type \u0026#39;int32\u0026#39;, got unconvertible type \u0026#39;string\u0026#39; * \u0026#39;[0].uid\u0026#39; expected type \u0026#39;int32\u0026#39;, got unconvertible type \u0026#39;string\u0026#39; 提示类型解析失败。其实，这种形式的 json 在其他一些软类型语言中也会出现。\n那如何解决这个问题？提两种解决方案\n使用时进行转化，比如类型为 int 的数据，使用时可以用 strconv.Atoi 转化。 使用 mapstructure 提供的软类型 map 转化 struct 的功能； 显然，第一种方式太 low，转化的时候还要多一步错误检查。那第二种方式如何呢？\n来看示例代码，如下：\nvar blogs []Blog if err := mapstructure.WeakDecode(e.Data, \u0026amp;blogs); err != nil { panic(err) } fmt.Println(blogs) 其实只需要把 mapstructure 的 Decode 替换成 WeakDecode 就行了，字如其意，弱解析。如此easy。\n到此，才算完成！接下来的数据处理就简单很多了。如果想学习 mapstructure 的使用，敲敲源码中例子应该差不多了。\n总结 # 本文由一个问题引出主题，如何处理不确定结构的 json 数据，开头提出了三种可行的解决方案，三种方案是逐层递进的。最终的方式需要依赖反射实现，当然同样的问题别人早就想到了，并开发了一个第三方包，mapstructure。\n最后，本文通过一个实际的案例演示了 mapstructure 的使用。\n感谢阅读，希望本文对你有所帮助。\n我的博文：Go 中如何解析 json 内部结构不确定的情况\n","date":"2019-10-17","externalUrl":null,"permalink":"/posts/2019-10-17-parse-dynamic-json-into-a-structure/","section":"文章","summary":"本文主要介绍的是关于 Go 如何解析 json 内部结构不确定的情况。\n首先，我们直接看一个来提问吧。\n问题如下：\n上游传递不确定的json，如何透传给下游业务？比如，我解析参数\n","title":"Go 中如何解析 json 内部结构不确定的情况","type":"posts"},{"content":"最近，我在尝试整理一篇关于 Go 包管理发展历史的文章，希望能加深自己对这一块知识的认识。在搜集资料的时候，发现了这篇文章，顺手翻译了一下。\n本文是该系列的第一篇，主要介绍包依赖管理中一些基础知识。文中提出了 Go 开发中的三个痛点，如何解决只能在 GOPATH 指定路径开发，如何实现有效的版本管理，以及如何支持 Go 原生工具集依赖管理。针对它们，Go Module 都提供了相应的解决方案。\n从第一篇的内容上看，作者后面的文章应该会对 Go 的模块机制进行详细的剖析，很期待。话说，总感觉这篇文章翻译的有点别扭，检查的时候发现有好几处语义理解错误，尴尬。\n翻译正文如下：\n介绍 # Go Module 是 Go 为包依赖管理提供的一个综合性解决方案。从 Go 初版发布以来，Go 开发者针对包管理这一块提出过三个痛点问题。\n如何实现在 GOPATH 工作区之外进行代码开发；\n如何实现依赖版本化管理和有效识别出使用依赖的兼容性问题；\n如何实现通过 Go 原生工具进行依赖管理；\n随着 Go 1.13 的发布，这三个问题都得到了解决。在过去的两年里，Go 团队成员为此付出了巨大的努力。本文中将重点介绍从 GOPATH 到模块机制的变化，还有模块究竟解决了什么问题。我将通过足够易懂的语言向大家说明模块的工作机制。\n我觉得，重点要理解为什么模块这样工作。\nGOPATH # GOPATH 是用于指定 Go 工作区的物理位置，一直以来都很好地服务着 Go 的开发者们。但它对非 Go 开发者并不友好，想在没有任何配置的情况下，随时随地进行 Go 开发，这是不可能的一件事。\nGo 团队要解决的第一个问题就是允许 Go 的源码仓库能被 clone 在磁盘中的任意位置，而不仅仅是 GOPATH 指定的工作区。并且 Go 工具集仍然要能成功定位、编译构建与测试它们。\n上图展示了一个 github 仓库，ardanlabs/conf，这个仓库仅有一个包，它用于提供对应用配置处理的支持。\n以前，如果想使用这个包，我们需要通过 go get 并指定仓库的规范化名称实现下载一份到你的 GOPATH 下。仓库规范化的名称是由远程仓库的基础 url 和仓库名称两部分组成。\n一个例子，在 Go Module 之前，如果你执行 go get github.com/ardanlabs/conf，代码将会被 clone 到 $GOPATH/src/github.com/ardanlabs/conf 目录下。基于 GOPATH 和仓库名，无论我们把工作区设置何处，Go 工具集始终都能正确地找到代码的位置。\n导入解析 # 清单 1\ngithub.com/ardanlabs/conf/blob/master/conf_test.go\npackage conf_test import ( ... \u0026#34;github.com/ardanlabs/conf\u0026#34; ... ) 清单1 显示了 conf 包中测试文件 conf_test.go 中的导入其他包的代码片段。\n当测试包名用 _test 命名，这就意味着测试代码和被测试代码是在不同的包中，测试代码必须导入要被测试的外部代码。从上面的代码片段中，我们可以看出，测试代码是如何将 conf 导入的。基于 GOPATH 机制，可以非常容易地解析出导入包的路径。然后，Go 工具集就可以成功定位、编译和测试代码。\n如果 GOPATH 不存在或者目录结构与仓库名称不匹配，将会如何呢？\n清单 2\nimport \u0026#34;github.com/ardanlabs/conf\u0026#34; // GOPATH mode: Physical location on disk matches the GOPATH // and Canonical name of the repo. // GOPATH 模式：磁盘物理位置与 GOPATH 和仓库的规范名称相匹配 $GOPATH/src/github.com/ardanlabs/conf // Module mode: Physical location on disk doesn’t represent // the Canonical name of the repo. // Module 模式：磁盘上的物理位置和仓库全名没有必然的匹配关系。 /users/bill/conf 清单2 展示了如果把仓库 clone 到任意位置将会产生什么问题。当开发者选择将代码下载他们希望的任意位置时，通过 import 包名称解析出源码的实际位置就不行了。\n如何解决这个问题？\n我们可以指定一个特殊的文件，使用它指定仓库的规范名称。这个文件的位置可理解为是 GOPATH 的一个替代，在它其中定义了仓库的规范名称，Go 工具可以通过这个名称解析源码中导入包的位置，而不必关心仓库被 clone 到了什么地方。\n我们把这个特殊的文件命名为 go.mod，将在这个文件中定义的由规范名称表示的新实体称为 Module。\n清单 3\ngithub.com/ardanlabs/conf/blob/v1.1.0/go.mod\nmodule github.com/ardanlabs/conf 清单3 中显示了 conf 仓库中的 go.mod 文件的第一行 。\n这一行定义了模块的名称，它同时也代表了仓库全名，开发者期待使用它来引用库中任意部分的代码。现在，库被下载到什么位置已经不再那么重要了，Go 工具集会根据 module 文件所在位置和模块名定位和解析内部包的导入，比如前面的示例中，在测试文件中的导入 conf 包。\n现在，模块机制允许我们将代码下载到任意位置。那下一个要解决的问题就是如何将代码捆绑到一起进行版本控制。\n捆绑和版本控制 # 多数的版本管理系统都支持了在任意提交点打标签。这些标签通常是被用来发布新特性（v1.0.0、v2.3.8，等等），而且一般都是不可变的。\n图中显示，conf 已经被打了三个不同的版本标签。这三个标签遵循着语义化版本的格式。\n利用版本管理工具，我们可以通过指定 tag 实现 clone 任意版本的 conf 包的目的。但这有两个问题亟待解决。\n我应该使用哪个版本的包； 我如何才能知道哪个版本的包兼容我所写的或使用的代码； 一旦回答完这两个问题，又会产生第三个问题：\n从哪里下载依赖的代码，Go 工具要能查找和访问到它； 接着，情况变得更差。\n为了要使用特定版本的 conf 包，你必须要下载 conf 的所有依赖。对于所有存在依赖传递的项目，这是一个共性的问题。\n在 GOPATH 模式下，可以使用 go get 识别和下载所有的依赖包，然后放到 GOPATH 指定的工作区下。但这不是一个完美的方案，因为 go get 仅仅只能从 master 分支下载和更新最新的代码。当初期写代码时，从 master 下载代码没什么问题。但几个月后，有些依赖可能已经升级了，master 分支的最新代码可能已经不再兼容你的项目。这是因为你的项目没有遵守明确的版本管理，任何的升级都可能带来一个不兼容的改变。\n在 Module 模式下，通过 go get 下载所有的依赖到一个单一的工作区不再是首选方式。你需要一种方式实现为整个项目中的每个依赖指定一个兼容版本。同时，还要支持针对同一个依赖不同主版本的引入，以防止出现一个项目中依赖同一个包的不同主版本。\n针对上面的这些问题，社区已经开发了一些解决方案，如 dep, godep, glide 等。但 Go 需要一个集成的解决方案。这个方案通过重用 go.mod 文件实现按版本维护这些直接和间接依赖。然后，将任何一个版本的依赖当成一个不可变的代码包。这个特定版本不可变的代码包被称为一个 Module。\n集成解决方案 # 上图显示了仓库和模块的关系。它显示了如何引用到一个特定版本模块中的包。在这种情况下，在 conf-1.1.0 的代码从版本为 0.3.1 的 go-cmp 导入了 cmp 包。既然，依赖信息已经在 conf 模块中（保存在模块文件中），Go 就可以通过内置的工具集获取指定版本的模块进行编译构建。\n一旦有了模块，许多便利的工程体验就体现了出来：\n可以向全世界的 Go 开发者提供支持，如 build、retain、authenticate, validate, fetch, cache 等； 在不同的版本管理系统前构建一个代理服务器，从而实现前面提到的那些支持； 可以验证一个模块是否被修改过，而不用关心它被构建了多少次，从何处何人手里获取， 在这方面是非常值得庆幸地，因为在 Go 1.13 中，Go 团队已经提供了许多这方面的支持。\n总结 # 这篇文章尝试为后面讨论 Go 模块是什么以及 Go 团队如何设计了这个方案打下了基础。接下来还有一些问题需要讨论，比如：\n一个特定版本的模块是如何被选择？ 模块文件是什么样的组织结构以及它提供了哪些选项帮助你控制模块的选择？ 模块是如何编译、获取和缓存到本地的磁盘帮助实现导入包的解析？ 如何通过语义版本进行模块验证？ 如何在你的项目中使用模块以及有什么最佳实践？ 在接下来的文章中，我计划将针对这些问题提供一个更深度的理解。现在，你要确保自己已经明白了仓库、包和模块之间的关系。\n我的博文：Go Module 存在的意义与解决的问题，译：Modules Part 01: Why And What\n","date":"2019-10-14","externalUrl":null,"permalink":"/posts/2019-10-14-gomod-what-andy-why/","section":"文章","summary":"最近，我在尝试整理一篇关于 Go 包管理发展历史的文章，希望能加深自己对这一块知识的认识。在搜集资料的时候，发现了这篇文章，顺手翻译了一下。\n本文是该系列的第一篇，主要介绍包依赖管理中一些基础知识。文中提出了 Go 开发中的三个痛点，如何解决只能在 GOPATH 指定路径开发，如何实现有效的版本管理，以及如何支持 Go 原生工具集依赖管理。针对它们，Go Module 都提供了相应的解决方案。\n","title":"Go Module 存在的意义与解决的问题","type":"posts"},{"content":"今天，尝试谈下 Go 中的引用。\n之所以要谈它，一方面是之前的我也有些概念混乱，想梳理下，另一方面是因为很多人对引用都有疑问。我经常会看到与引用有关的问题。\n比如，什么是引用？引用和指针有什么区别？Go 中有引用类型吗？什么是值传递？址传递？引用传递？\n在开始谈论之前，我已经感觉到这必定是一个非常头疼的话题。这或许就是学了那么多语言，但没有深入总结，从而导致的思维混乱。\n前言 # 我的理解是，要彻底搞懂引用，得从类型和传递两个角度分别进行思考。\n从类型角度，类型可分为值类型和引用类型，一般而言，我们说到引用，强调的都是类型。\n从传递角度，有值传递、址传递和引用传递，传递是在函数调用时才会提到的概念，用于表明实参与形参的关系。\n引用类型和引用传递的关系，我尝试用一句话概括，引用类型不一定是引用传递，但引用传递的一定是引用类型。\n这几句话，是我在使用各种语言的之后总结出来的，希望无误吧，毕竟不能误导他人。\n是什么 # 谈到引用，就不得不提指针，而指针与引用是编程学习中老生常谈的话题了。有些编程语言为了降低程序员的使用门槛，只有引用。而有些语言则是指针引用皆存在，如 C++ 和 Go。\n指针，即地址的意思。\n在程序运行的时候，操作系统会为每个变量分配一块内存放变量内容，而这块内存有一个编号，即内存地址，也就是变量的地址。现在 CPU 一般都是 64 位，因而，这个地址的长度一般也就是 8 个字节。\n引用，某块内存的别名。\n一般情况，都会这么解释引用。换句话说，引用代指某个内存地址，这句话真的是非常简洁，同时也非常好理解。但在 Go 中，这句话看起来并不全面，具体后面解释。\n除了指针和引用，还有另外一个更广泛的概念，值。谈变量传递时，常会提到值传递、址传递和引用传递。从广义上看，对大部分的语言而言，指针和引用都属于值。而从狭义角度来说，则可分为值、址和引用。\n相当绕人是不是？\n我已经感觉到自己头发在掉了。其实，要想彻底搞清楚这些概念，还是得从本质出发。\n值和指针 # 先来搞明白值与指针区别。\n上一节在介绍指针的时候，提到了要注意变量的地址和内容的不同。为什么要说这句话呢？\n假设，我们定义一个 int 类型的变量 a，如下：\nvar a int = 1 变量 a 的内容为 1，而变量内容是存在某个地址之中的。如何获取变量地址呢？Go 中获取变量地址的方法与 C/C++ 相同。代码如下：\nvar p = \u0026amp;a 通过 \u0026amp; 获取 a 的地址。同时，这里还定义了一个新的变量 p 用于保存变量 a 的地址。p 的类型为 int 指针，也就是变量 p 中的内容是变量 a 的地址。\n如下代码输出它们的地址：\nvar a = 1 var p = \u0026amp;a fmt.Printf(\u0026#34;%p\\n\u0026#34;, p) fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;p) 我这里的输出结果是，变量 a 和 p 的地址分别为 0xc000092000 和 0xc00008c010。此时的内存的分布如下：\n变量 p 的内容是 a 的地址，因而可以说指针即是其他变量的内容，也是某个变量的地址。为什么啰啰嗦嗦的说这些，因为在学习 C 语言，会单独强调址的概念，但在 Go 中，指针相对弱化，也是归于值类型之中。\n引用的本质 # 前面说过，引用是某块内存的别名。从字面理解，似乎表达的是引用类型变量中的内容是指针，这么理解似乎也没错。既然如此，我自然而然地想到，怎么将引用与指针关联起来。\n在 C/C++ 中，引用其实是编译器实现的一个语法糖，经过汇编后，将会把引用操作转化为了指针操作。这真的是别名啊，有种 define 预处理的感觉，只不过是汇编级别的。分享一篇 C++中“引用”的底层实现 的文章，有兴趣仔细读读，我只是看了个大概。\n而其他一些语言中，引用的本质其实是 struct 中包含指针，比如 Python。下面的 C 结构是 Python 中列表类型的底层结构。\ntypedef struct { PyObject_VAR_HEAD PyObject **ob_item; Py_ssize_t allocated; } PyListObject; 变量真正存放数据的地方在 **ob_item 中。结构中的其他两个成员起辅助作用。\n现在看来，引用的实现主要有两种。一是 C++ 的思路，引用其实一种便于使用指针的语法糖，和我们想象中的别名含义一致。二是类似 Python 中的实现，底层结构中包含指向实际内容的指针。\n当然，或许还有其他的实现方式，但核心应该是不变的。\n引用传递 # 谈到引用传递，就不得不提值传递，值传递的一般定义如下。\n函数调用时，实参通过拷贝将自身内容传递给形参，形参实际上是实参值的一个拷贝，此时，针对函数中形参的任何操作，仅仅是针对实参的副本，不影响原始值的内容。\n值传递中有一个特殊形式，如果传递参数的类型是指针，我们就会称之为址传递，C 语言中就有值传递和址传递两种说法。深究起来，C 中的址传递也属于值传递，因为对指针类型而言，变量的值是指针，即传递的值也是指针。而 C 语言之所以强调址传递，我认为主要 C 这门底层语言对指针较为重视。\n什么是引用传递？\n参考值传递的定义，实参地址在函数调用被传递给形参，针对形参的操作，影响到了实参，则可以认为是引用传递。\n在我用过的语言中，支持引用传递的语言有 PHP 和 C++。\nGo 的引用实现 # Go 的引用类型有 slice、map 和 chan，实现机制采用的是前面提到的第二种方式，即结构体含指针成员。它们都可以使用内置函数 make 进行初始化。\n原本我是想把这几种引用类型的底层结构都贴出来，但发现这会干扰本文主题的理解。我们只看 slice 的结构，如下：\n// slice type slice struct { array unsafe.Pointer len int cap int } slice 的结构最简单，包含三个成员，分别是切片的底层数组地址、切片长度和容量大小。是否感觉与前面提到的 Python 列表的底层结构非常类似？\n如果想了解 map 和 chan 的结构，可自行阅读 go 的源码，runtime/slice.go、runtime/map.go 和 runtime/chan.go。\n如果不想研究源码，推荐阅读饶大的 Go 深度解密系列文章，包括 深度解密Go语言之Slice、深度解密Go语言之map、深度解密Go语言之channel，这几篇文章因为写的都非常细且非常长，可能读起来会比较考验你的耐心。\nGo 是值传递 # 按官方说法，Go 中只有值传递。原文如下：\nIn a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.\n重点是下面这句话。\nAfter they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.\n有点迷糊？最初我也迷糊，Go 不是有指针和引用类型吗。但读了一些文章，思考了许久，才彻底想明白。下面，我将尝试为官方的说法找个合理的解释。\n为什么说 Go 中没有址传递\n其实，这个问题前面已经解释的很清楚了，指针只是值的一种特殊形式，C 语言是门非常底层的语言，常会涉及一些地址操作，会强调指针的特殊地位。但于 Go 而言，指针已经弱化了很多，Go 团队可能也觉得没有必要再单独强调指针的地位。\n为什么说 Go 中没有引用传递？\n有人可能会说，Go 中明明有引用传递，按照引用传递的定义，可以非常容易就拿出一个例子反驳我。\npackage main import \u0026#34;fmt\u0026#34; func update(s []int) { s[1] = 10 } func main() { a := []int{0, 1, 2, 3, 4} fmt.Println(a) update(a) fmt.Println(a) } 输出结果如下：\n[0 1 2 3 4] [0 10 2 3 4] 针对形参 s 的操作确实改变了实参 a 的值，似乎的确是引用传递。但我想说的是，针对形参的操作并非指的是针对形参中某个元素的操作。\n看个 C++ 中引用的例子。\nvoid update(int\u0026amp; s) { s = 10; printf(\u0026#34;s address: %p\\n\u0026#34;, \u0026amp;s); } int main() { int a = 1; std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; std::endl; printf(\u0026#34;a address: %p\\n\u0026#34;, \u0026amp;a); update(a); std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; std::endl; } 执行结果如下：\n1 a address: 0x7fff5b98f21c s address: 0x7fff5b98f21c 10 针对 s 的操作确实改变了 a 的值。在 Go 中尝试同样的代码，如下：\nfunc update(s []int) { s[1] = 10 fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;s) } func main() { a := []int{0, 1, 2, 3, 4} fmt.Println(a) fmt.Printf(\u0026#34;%p\\n\u0026#34;, \u0026amp;a) update(a) fmt.Println(a) } 输出如下：\n[0 1 2 3 4] 0xc00000c060 0xc000098000 [0 10 2 3 4] 非常遗憾，针对形参的赋值操作并没有改变实参的值。基于此，得出结论是 slice 的传递并非引用传递。我比较喜欢的这种解释方式，适合我个人的记忆理解，不知道是否有不妥的地方。\n除此之外，介绍另外一种识别是否是引用传递的方式。\n通过比较形参和实参地址确认，如果两者地址相同，则是引用传递，不同则非引用传递。但因为 C++ 和 Go 引用的实现机制不同，理解起来会比较困难。我们也可以选择只记结论。\n这种方式的验证非常简单，我们在上面的 C++ 和 Go 的例子中已经输出了形参和实参的地址，比较下即可得出结论。\n总结 # 本文主要从引用的类型和传递两个角度出发，深入浅出的分析了 Go 中的引用。\n首先，引用类型和引用传递并没有绝对的关系，不知道有多少人认为引用类型必然是引用传递。接着，我们讨论了不同语言引用的实现机制，涉及到 C++、Python 和 Go。\n文章的最后，解释了一个常见的疑惑，为什么说 Go 只有值传递。在此基础上，文中提出了两种方式，帮助识别一门语言是否支持引用传递。\n相关阅读 # golang中哪些引用类型的指针在声明时不用加\u0026amp;号，哪些在函数定义的形参和返回值类型中不用*号标注\nGolang中的make(T, args)为什么返回T而不是*T?\nGo语言参数传递是传值还是传引用\nGolang中函数传参存在引用传递吗？\nC++ 引用 底层实现机制\nThe Go Programming Language Specification\n我的博文：一文理清 Go 引用的常见疑惑\n","date":"2019-09-28","externalUrl":null,"permalink":"/posts/2019-09-28-understand-golang-reference/","section":"文章","summary":"今天，尝试谈下 Go 中的引用。\n之所以要谈它，一方面是之前的我也有些概念混乱，想梳理下，另一方面是因为很多人对引用都有疑问。我经常会看到与引用有关的问题。\n","title":"一文理清 Go 引用的常见疑惑","type":"posts"},{"content":"之前在知乎看到一个问题：为什么 Golang 没有像 Python 中 in 一样的功能？于是，搜了下这个问题，发现还是有不少人有这样的疑问。\n今天来谈谈这个话题。\nin 是一个很常用的功能，有些语言中可能也称为 contains，虽然不同语言的表示不同，但基本都是有的。不过可惜的是，Go 却没有，它即没有提供类似 Python 操作符 in，也没有像其他语言那样提供这样的标准库函数，如 PHP 中 in_array。\nGo 的哲学是追求少即是多。我想或许 Go 团队觉得这是一个实现起来不足为道的功能吧。\n为何说微不足道？如果要自己实现，又该如何做呢？\n我所想到的有三种实现方式，一是遍历，二是 sort 的二分查找，三是 map 的 key 索引。\n本文相关源码已经上传在我的 github 上，poloxue/gotin。\n遍历 # 遍历应该是我们最容易想到的最简单的实现方式。\n示例如下：\nfunc InIntSlice(haystack []int, needle int) bool { for _, e := range haystack { if e == needle { return true } } return false } 上面演示了如何在一个 []int 类型变量中查找指定 int 是否存在的例子，是不是非常简单，由此我们也可以感受到我为什么说它实现起来微不足道。\n这个例子有个缺陷，它只支持单一类型。如果要支持像解释语言一样的通用 in 功能，则需借助反射实现。\n代码如下：\nfunc In(haystack interface{}, needle interface{}) (bool, error) { sVal := reflect.ValueOf(haystack) kind := sVal.Kind() if kind == reflect.Slice || kind == reflect.Array { for i := 0; i \u0026lt; sVal.Len(); i++ { if sVal.Index(i).Interface() == needle { return true, nil } } return false, nil } return false, ErrUnSupportHaystack } 为了更加通用，In 函数的输入参数 haystack 和 needle 都是 interface{} 类型。\n简单说说输入参数都是 interface{} 的好处吧，主要有两点，如下：\n其一，haystack 是 interface{} 类型，使 in 支持的类型不止于 slice，还包括 array。我们看到，函数内部通过反射对 haystack 进行了类型检查，支持 slice（切片）与 array（数组）。如果是其他类型则会提示错误，增加新的类型支持，如 map，其实也很简单。但不推荐这种方式，因为通过 _, ok := m[k] 的语法即可达到 in 的效果。\n其二，haystack 是 interface{}，则 []interface{} 也满足要求，并且 needle 是 interface{}。如此一来，我们就可以实现类似解释型语言一样的效果了。\n怎么理解？直接示例说明，如下：\ngotin.In([]interface{}{1, \u0026#34;two\u0026#34;, 3}, \u0026#34;two\u0026#34;) haystack 是 []interface{}{1, \u0026ldquo;two\u0026rdquo;, 3}，而且 needle 是 interface{}，此时的值是 \u0026ldquo;two\u0026rdquo;。如此看起来，是不是实现了解释型语言中，元素可以是任意类型，不必完全相同效果。如此一来，我们就可以肆意妄为的使用了。\n但有一点要说明，In 函数的实现中有这样一段代码：\nif sVal.Index(i).Interface() == needle { ... } Go 中并非任何类型都可以使用 == 比较的，如果元素中含有 slice 或 map，则可能会报错。\n二分查找 # 以遍历确认元素是否存在有个缺点，那就是，如果数组或切片中包含了大量数据，比如 1000000 条数据，即一百万，最坏的情况是，我们要遍历 1000000 次才能确认，时间复杂度 On。\n有什么办法可以降低遍历次数？\n自然而然地想到的方法是二分查找，它的时间复杂度 log2(n) 。但这个算法有前提，需要依赖有序序列。\n于是，第一个要我们解决的问题是使序列有序，Go 的标准库已经提供了这个功能，在 sort 包下。\n示例代码如下：\nfmt.Println(sort.SortInts([]int{4, 2, 5, 1, 6})) 对于 []int，我们使用的函数是 SortInts，如果是其他类型切片，sort 也提供了相关的函数，比如 []string 可通过 SortStrings 排序。\n完成排序就可以进行二分查找，幸运的是，这个功能 Go 也提供了，[]int 类型对应函数是 SearchInts。\n简单介绍下这个函数，先看定义：\nfunc SearchInts(a []int, x int) int 输入参数容易理解，从切片 a 中搜索 x。重点要说下返回值，这对于我们后面确认元素是否存在至关重要。返回值的含义，返回查找元素在切片中的位置，如果元素不存在，则返回，在保持切片有序情况下，插入该元素应该在什么位置。\n比如，序列如下：\n1 2 6 8 9 11 假设，x 为 6，查找之后将发现它的位置在索引 2 处；x 如果是 7，发现不存在该元素，如果插入序列，将会放在 6 和 8 之间，索引位置是 3，因而返回值为 3。\n代码测试下：\nfmt.Println(sort.SearchInts([]int{1, 2, 6, 8, 9, 11}, 6)) // 2 fmt.Println(sort.SearchInts([]int{1, 2, 6, 8, 9, 11}, 7)) // 3 如果判断元素是否在序列中，只要判断返回位置上的值是否和查找的值相同即可。\n但还有另外一种情况，如果插入元素位于序列最后，例如元素值为 12，插入位置即为序列的长度 6。如果直接查找 6 位置上的元素就可能发生越界的情况。那怎么办呢？其实判断返回是否大于切片长度即可，大于则说明元素不在切片序列中。\n完整的实现代码如下：\nfunc SortInIntSlice(haystack []int, needle int) bool { sort.Ints(haystack) index := sort.SearchInts(haystack, needle) return index \u0026lt; len(haystack) \u0026amp;\u0026amp; haystack[index] == needle } 但这还有个问题，对于无序的场景，如果每次查询都要经过一次排序并不划算。最后能实现一次排序，稍微修改下代码。\nfunc InIntSliceSortedFunc(haystack []int) func(int) bool { sort.Ints(haystack) return func(needle int) bool { index := sort.SearchInts(haystack, needle) return index \u0026lt; len(haystack) \u0026amp;\u0026amp; haystack[index] == needle } } 上面的实现，我们通过调用 InIntSliceSortedFunc 对 haystack 切片排序，并返回一个可多次使用的函数。\n使用案例如下：\nin := gotin.InIntSliceSortedFunc(haystack) for i := 0; i\u0026lt;maxNeedle; i++ { if in(i) { fmt.Printf(\u0026#34;%d is in %v\u0026#34;, i, haystack) } } 二分查找的方式有什么不足呢？\n我想到的重要一点，要实现二分查找，元素必须是可排序的，如 int，string，float 类型。而对于结构体、切片、数组、映射等类型，使用起来就不是那么方便，当然，如果要用，也是可以的，不过需要我们进行一些适当扩展，按指定标准排序，比如结构的某个成员。\n到此，二分查找的 in 实现就介绍完毕了。\nmap key # 本节介绍 map key 方式。它的算法复杂度是 O1，无论数据量多大，查询性能始终不变。它主要依赖的是 Go 中的 map 数据类型，通过 hash map 直接检查 key 是否存在，算法大家应该都比较熟悉，通过 key 可直接映射到索引位置。\n我们常会用到这个方法。\n_, ok := m[k] if ok { fmt.Println(\u0026#34;Found\u0026#34;) } 那么它和 in 如何结合呢？一个案例就说明白了这个问题。\n假设，我们有一个 []int 类型变量，如下：\ns := []int{1, 2, 3} 为了使用 map 的能力检查某个元素是否存在，可以将 s 转化 map[int]struct{}。\nm := map[interface{}]struct{}{ 1: struct{}{}, 2: struct{}{}, 3: struct{}{}, 4: struct{}{}, } 如果检查某个元素是否存在，只需要通过如下写法即可确定：\nk := 4 if _, ok := m[k]; ok { fmt.Printf(\u0026#34;%d is found\\n\u0026#34;, k) } 是不是非常简单？\n补充一点，关于这里为什么使用 struct{}，可以阅读我之前写的一篇关于 Go 中如何使用 set 的文章。\n按照这个思路，实现函数如下：\nfunc MapKeyInIntSlice(haystack []int, needle int) bool { set := make(map[int]struct{}) for _ , e := range haystack { set[e] = struct{}{} } _, ok := set[needle] return ok } 实现起来不难，但和二分查找有着同样的问题，开始要做数据处理，将 slice 转化为 map。如果是每次数据相同，稍微修改下它的实现。\nfunc InIntSliceMapKeyFunc(haystack []int) func(int) bool { set := make(map[int]struct{}) for _ , e := range haystack { set[e] = struct{}{} } return func(needle int) bool { _, ok := set[needle] return ok } } 对于相同的数据，它会返回一个可多次使用的 in 函数，一个使用案例如下：\nin := gotin.InIntSliceMapKeyFunc(haystack) for i := 0; i\u0026lt;maxNeedle; i++ { if in(i) { fmt.Printf(\u0026#34;%d is in %v\u0026#34;, i, haystack) } } 对比前两种算法，这种方式的处理效率最高，非常适合于大数据的处理。接下来的性能测试，我们将会看到效果。\n性能 # 介绍完所有方式，我们来实际对比下每种算法的性能。测试源码位于 gotin_test.go 文件中。\n基准测试主要是从数据量大小考察不同算法的性能，本文中选择了三个量级的测试样本数据，分别是 10、1000、1000000。\n为便于测试，首先定义了一个用于生成 haystack 和 needle 样本数据的函数。\n代码如下：\nfunc randomHaystackAndNeedle(size int) ([]int, int){ haystack := make([]int, size) for i := 0; i\u0026lt;size ; i++{ haystack[i] = rand.Int() } return haystack, rand.Int() } 输入参数是 size，通过 rand.Int() 随机生成切片大小为 size 的 haystack 和 1 个 needle。在基准测试用例中，引入这个随机函数生成数据即可。\n举个例子，如下：\nfunc BenchmarkIn_10(b *testing.B) { haystack, needle := randomHaystackAndNeedle(10) b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { _, _ = gotin.In(haystack, needle) } } 首先，通过 randomHaystackAndNeedle 随机生成了一个含有 10 个元素的切片。因为生成样本数据的时间不应该计入到基准测试中，我们使用 b.ResetTimer() 重置了时间。\n其次，压测函数是按照 Test+函数名+样本数据量 规则编写，如案例中 BenchmarkIn_10，表示测试 In 函数，样本数据量为 10。如果我们要用 1000 数据量测试 InIntSlice，压测函数名为 BenchmarkInIntSlice_1000。\n测试开始吧！简单说下我的笔记本配置，Mac Pro 15 版，16G 内存，512 SSD，4 核 8 线程的 CPU。\n测试所有函数在数据量在 10 的情况下的表现。\n$ go test -run=none -bench=10$ -benchmem 匹配所有以 10 结尾的压测函数。\n测试结果：\ngoos: darwin goarch: amd64 pkg: github.com/poloxue/gotin BenchmarkIn_10-8 3000000 501 ns/op 112 B/op 11 allocs/op BenchmarkInIntSlice_10-8 200000000 7.47 ns/op 0 B/op 0 allocs/op BenchmarkInIntSliceSortedFunc_10-8 100000000 22.3 ns/op 0 B/op 0 allocs/op BenchmarkSortInIntSlice_10-8 10000000 162 ns/op 32 B/op 1 allocs/op BenchmarkInIntSliceMapKeyFunc_10-8 100000000 17.7 ns/op 0 B/op 0 allocs/op BenchmarkMapKeyInIntSlice_10-8 3000000 513 ns/op 163 B/op 1 allocs/op PASS ok github.com/poloxue/gotin 13.162s 表现最好的并非 SortedFunc 和 MapKeyFunc，而是最简单的针对单类型的遍历查询，平均耗时 7.47ns/op，当然，另外两种方式表现也不错，分别是 22.3ns/op 和 17.7ns/op。\n表现最差的是 In、SortIn（每次重复排序） 和 MapKeyIn（每次重复创建 map）两种方式，平均耗时分别为 501ns/op 和 513ns/op。\n测试所有函数在数据量在 1000 的情况下的表现。\n$ go test -run=none -bench=1000$ -benchmem 测试结果：\ngoos: darwin goarch: amd64 pkg: github.com/poloxue/gotin BenchmarkIn_1000-8 30000 45074 ns/op 8032 B/op 1001 allocs/op BenchmarkInIntSlice_1000-8 5000000 313 ns/op 0 B/op 0 allocs/op BenchmarkInIntSliceSortedFunc_1000-8 30000000 44.0 ns/op 0 B/op 0 allocs/op BenchmarkSortInIntSlice_1000-8 20000 65401 ns/op 32 B/op 1 allocs/op BenchmarkInIntSliceMapKeyFunc_1000-8 100000000 17.6 ns/op 0 B/op 0 allocs/op BenchmarkMapKeyInIntSlice_1000-8 20000 82761 ns/op 47798 B/op 65 allocs/op PASS ok github.com/poloxue/gotin 11.312s 表现前三依然是 InIntSlice、InIntSliceSortedFunc 和 InIntSliceMapKeyFunc，但这次顺序发生了变化，MapKeyFunc 表现最好，17.6 ns/op，与数据量 10 的时候相比基本无变化。再次验证了前文的说法。\n同样的，数据量 1000000 的时候。\n$ go test -run=none -bench=1000000$ -benchmem 测试结果如下：\ngoos: darwin goarch: amd64 pkg: github.com/poloxue/gotin BenchmarkIn_1000000-8 30 46099678 ns/op 8000098 B/op 1000001 allocs/op BenchmarkInIntSlice_1000000-8 3000 424623 ns/op 0 B/op 0 allocs/op BenchmarkInIntSliceSortedFunc_1000000-8 20000000 72.8 ns/op 0 B/op 0 allocs/op BenchmarkSortInIntSlice_1000000-8 10 138873420 ns/op 32 B/op 1 allocs/op BenchmarkInIntSliceMapKeyFunc_1000000-8 100000000 16.5 ns/op 0 B/op 0 allocs/op BenchmarkMapKeyInIntSlice_1000000-8 10 156215889 ns/op 49824225 B/op 38313 allocs/op PASS ok github.com/poloxue/gotin 15.178s MapKeyFunc 依然表现最好，每次操作用时 17.2 ns，Sort 次之，而 InIntSlice 呈现线性增加的趋势。一般情况下，如果不是对性能要特殊要求，数据量特别大的场景，针对单类型的遍历已经有非常好的性能了。\n从测试结果可以看出，反射实现的通用 In 函数每次执行需要进行大量的内存分配，方便的同时，也是以牺牲性能为代价的。\n总结 # 本文通过一个问题引出主题，为什么 Go 中没有类似 Python 的 In 方法。我认为，一方面是实现非常简单，没有必要。除此以外，另一方面，在不同场景下，我们还需要根据实际情况分析用哪种方式实现，而不是一种固定的方式。\n接着，我们介绍了 In 实现的三种方式，并分析了各自的优劣。通过性能分析测试，我们能得出大致的结论，什么方式适合什么场景，但总体还是分析的不够细致，有兴趣的朋友可以继续研究下。\n博文地址：Go 中 Slice 的 In 实现探索\n参考 # Does Go have “if x in” construct similar to Python?\n为什么Golang没有像Python中in一样的功能？\n","date":"2019-09-15","externalUrl":null,"permalink":"/posts/2019-09-15-how-to-use-contain-function-in-golang/","section":"文章","summary":"之前在知乎看到一个问题：为什么 Golang 没有像 Python 中 in 一样的功能？于是，搜了下这个问题，发现还是有不少人有这样的疑问。\n今天来谈谈这个话题。\nin 是一个很常用的功能，有些语言中可能也称为 contains，虽然不同语言的表示不同，但基本都是有的。不过可惜的是，Go 却没有，它即没有提供类似 Python 操作符 in，也没有像其他语言那样提供这样的标准库函数，如 PHP 中 in_array。\n","title":"Go 中 Slice 的 In 实现探索","type":"posts"},{"content":"前几天在 \u0026ldquo;知乎想法\u0026rdquo; 谈到了一个话题，如何模仿学习，举了通过 net/http client 模仿 Pyhton 的requests的例子。但并未实践，难道想法真的只能是想法吗？当然不是，于是我决定先暂停一周 GO 笔记，来实践下自己的想法。\n有些新的知识，我们可以通过模仿学习\n本文将通过 GO 实现 requests 的 quick start 文档中的所有例子，系统学习http client的使用。虽然标题是 quick start，但其实内容挺多的。\n快速体验 # 首先，我们来发起一个 GET 请求，代码非常简单。如下：\nfunc get() { r, err := http.Get(\u0026#34;https://api.github.com/events\u0026#34;) if err != nil { panic(err) } defer func() { _ = r.Body.Close() }() body, _ := ioutil.ReadAll(r.Body) fmt.Printf(\u0026#34;%s\u0026#34;, body) } 通过 http.Get 方法，获取到了一个 Response 和一个 error ，即 r 和 err。通过 r 我们能获取响应的信息，err 可以实现错误检查。\nr.Body 被读取后需要关闭，可以defer来做这件事。内容的读取可通过 ioutil.ReadAll实现。\n请求方法 # 除了GET，HTTP还有其他一系列方法，包括POST、PUT、DELETE、HEAD、OPTIONS。快速体验中的GET是通过一种便捷的方式实现的，它隐藏了很多细节。这里暂时先不用它。\n我们先来介绍通用的方法，以帮我们实现所有HTTP方法的请求。主要涉及两个重要的类型，Client 和 Request。\nClient 即是发送 HTTP 请求的客户端，请求的执行都是由 Client 发起。它提供了一些便利的请求方法，比如我们要发起一个Get请求，可通过 client.Get(url) 实现。更通用的方式是通过 client.Do(req) 实现，req 属于 Request 类型。\nRequest 是用来描述请求信息的结构体，比如请求方法、地址、头部等信息，我们都可以通过它来设置。Request 的创建可以通过 http.NewRequest 实现。\n接下来列举 HTTP 所有方法的实现代码。\nGET\nr, err := http.DefaultClient.Do( http.NewRequest(http.MethodGet, \u0026#34;https://api.github.com/events\u0026#34;, nil)) POST\nr, err := http.DefaultClient.Do( http.NewRequest(http.MethodPost, \u0026#34;http://httpbin.org/post\u0026#34;, nil)) PUT\nr, err := http.DefaultClient.Do( http.NewRequest(http.MethodPut, \u0026#34;http://httpbin.org/put\u0026#34;, nil)) DELETE\nr, err := http.DefaultClient.Do( http.NewRequest(http.MethodDelete, \u0026#34;http://httpbin.org/delete\u0026#34;, nil)) HEAD\nr, err := http.DefaultClient.Do( http.NewRequest(http.MethodHead, \u0026#34;http://httpbin.org/get\u0026#34;, nil)) OPTIONS\nr, err := http.DefaultClient.Do( http.NewRequest(http.MethodOptions, \u0026#34;http://httpbin.org/get\u0026#34;, nil)) 上面展示了HTTP所有方法的实现。这里还几点需要说明。\nDefaultClient，它是 net/http 包提供了默认客户端，一般的请求我们无需创建新的 Client，使用默认即可。\nGET、POST 和 HEAD 的请求，GO提供了更便捷的实现方式，Request 不用手动创建。\n示例代码，每个 HTTP 请求方法都有两种实现。\nGET\nr, err := http.DefaultClient.Get(\u0026#34;http://httpbin.org/get\u0026#34;) r, err := http.Get(\u0026#34;http://httpbin.org/get\u0026#34;) POST\nbodyJson, _ := json.Marshal(map[string]interface{}{ \u0026#34;key\u0026#34;: \u0026#34;value\u0026#34;, }) r, err := http.DefaultClient.Post( \u0026#34;http://httpbin.org/post\u0026#34;, \u0026#34;application/json\u0026#34;, strings.NewReader(string(bodyJson)), ) r, err := http.Post( \u0026#34;http://httpbin.org/post\u0026#34;, \u0026#34;application/json\u0026#34;, strings.NewReader(string(bodyJson)), ) 这里顺便演示了如何向 POST 接口提交 JSON 数据的方式，主要 content-type 的设置，一般JSON接口的 content-type 为 application/json。\nHEAD\nr, err := http.DefaultClient.Head(\u0026#34;http://httpbin.org/get\u0026#34;) r, err := http.Head(\u0026#34;http://httpbin.org/get\u0026#34;) 如果看了源码，你会发现，http.Get 中调用就是 http.DefaultClient.Get，是同一个意思，只是为了方便，提供这种调用方法。Head 和 Post 也是如此。\nURL参数 # 通过将键/值对置于 URL 中，我们可以实现向特定地址传递数据。该键/值将跟在一个问号的后面，例如 http://httpbin.org/get?key=val。 手工构建 URL 会比较麻烦，我们可以通过 net/http 提供的方法来实现。\n举个栗子，比如你想传递 key1=value1 和 key2=value2 到 http://httpbin.org/get。代码如下：\nreq, err := http.NewRequest(http.MethodGet, \u0026#34;http://httpbin.org/get\u0026#34;, nil) if err != nil { panic(err) } params := make(url.Values) params.Add(\u0026#34;key1\u0026#34;, \u0026#34;value1\u0026#34;) params.Add(\u0026#34;key2\u0026#34;, \u0026#34;value2\u0026#34;) req.URL.RawQuery = params.Encode() // URL 的具体情况 http://httpbin.org/get?key1=value1\u0026amp;key2=value2 // fmt.Println(req.URL.String()) r, err := http.DefaultClient.Do(req) url.Values 可以帮助组织 QueryString，查看源码发现 url.Values 其实是 map[string][]string。调用 Encode 方法，将组织的字符串传递给请求 req 的 RawQuery。通过 url.Values也可以设置一个数组参数，类似如下的形式：\nhttp://httpbin.org/get?key1=value1\u0026key2=value2\u0026key2=value3\n怎么做呢？\nparams := make(url.Values) params.Add(\u0026#34;key1\u0026#34;, \u0026#34;value1\u0026#34;) params.Add(\u0026#34;key2\u0026#34;, \u0026#34;value2\u0026#34;) params.Add(\u0026#34;key2\u0026#34;, \u0026#34;value3\u0026#34;) 观察最后一行代码。其实，只要在 key2 上再增加一个值就可以了。\n响应信息 # 执行请求成功，如何查看响应信息。要查看响应信息，可以大概了解下，响应通常哪些内容？常见的有主体内容（Body）、状态信息（Status）、响应头部（Header）、内容编码（Encoding）等。\nBody # 其实，在最开始的时候已经演示Body读取的过程。响应内容的读取可通过 ioutil 实现。\nbody, err := ioutil.ReadAll(r.Body) 响应内容多样，如果是 json，可以直接使用 json.Unmarshal 进行解码，JSON知识不介绍了。\nr.Body 实现了 io.ReadeCloser 接口，为减少资源浪费要及时释放，可以通过 defer 实现。\ndefer func() { _ = r.Body.Close() }() StatusCode # 响应信息中，除了 Body 主体内容，还有其他信息，比如 status code 和 charset 等。\nr.StatusCode r.Status r.StatusCode 是 HTTP 返回码，Status 是返回状态描述。\nHeader # 响应头信息通过 Response.Header 即可获取，要说明的一点是，响应头的 Key 是不区分大小写。\nr.Header.Get(\u0026#34;content-type\u0026#34;) r.Header.Get(\u0026#34;Content-Type\u0026#34;) 你会发现 content-type 和 Content-Type 获取的内容是完全一样的。\nEncoding # 如何识别响应内容编码呢？我们需要借助 http://golang.org/x/net/html/charset 包实现。先来定义一个函数，代码如下：\nfunc determineEncoding(r *bufio.Reader) encoding.Encoding { bytes, err := r.Peek(1024) if err != nil { fmt.Printf(\u0026#34;err %v\u0026#34;, err) return unicode.UTF8 } e, _, _ := charset.DetermineEncoding(bytes, \u0026#34;\u0026#34;) return e } 怎么调用它？\nbodyReader := bufio.NewReader(r.Body) e := determineEncoding(bodyReader) fmt.Printf(\u0026#34;Encoding %v\\n\u0026#34;, e) decodeReader := transform.NewReader(bodyReader, e.NewDecoder()) 利用 bufio 生成新的 reader，然后利用 determineEncoding 检测内容编码，并通过 transform 进行编码转化。\n图片下载 # 如果访问内容是一张图片，我们如何把它下载下来呢？比如如下地址的图片。\nhttps://pic2.zhimg.com/v2-5e8b41cae579722bd6b8a612bf1660e6.jpg\n其实很简单，只需要创建新的文件并把响应内容保存进去即可。\nf, err := os.Create(\u0026#34;as.jpg\u0026#34;) if err != nil { panic(err) } defer func() { _ = f.Close() }() _, err = io.Copy(f, r.Body) if err != nil { panic(err) } r 即 Response，利用 os 创建了新的文件，然后再通过 io.Copy 将响应的内容保存进文件中。\n定制请求头 # 如何为请求定制请求头呢？Request 其实已经提供了相应的方法，通过 req.Header.Add 即可完成。\n举个例子，假设我们将要访问 http://httpbin.org/get，但这个地址针对 user-agent 设置了发爬策略。我们需要修改默认的 user-agent。\n示例代码：\nreq, err := http.NewRequest(http.MethodGet, \u0026#34;http://httpbin.org/get\u0026#34;, nil) if err != nil { panic(err) } req.Header.Add(\u0026#34;user-agent\u0026#34;, \u0026#34;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0)\u0026#34;) 如上便可完成任务。\n复杂的POST请求 # 前面已经展示过了向 POST 接口提交 JSON 数据的方式。接下来介绍下另外几种向 POST 接口提交数据的方式，即表单提交和文件提交。\n表单提交 # 表单提交是一个很常用的功能，故而在 net/http 中，除了提供标准的用法外，还给我们提供了简化的方法。\n我们先来介绍个标准的实现方法。\n举个例子，假设要向 http://httpbin.org/post 提交 name 为 poloxue 和 password 为 123456 的表单。\npayload := make(url.Values) payload.Add(\u0026#34;name\u0026#34;, \u0026#34;poloxue\u0026#34;) payload.Add(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;) req, err := http.NewRequest( http.MethodPost, \u0026#34;http://httpbin.org/post\u0026#34;, strings.NewReader(payload.Encode()), ) if err != nil { panic(err) } req.Header.Add(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/x-www-form-urlencoded\u0026#34;) r, err := http.DefaultClient.Do(req) POST 的 payload 是形如 name=poloxue\u0026amp;password=123456 的字符串，故而我们可以通过 url.Values 进行组织。\n提交给 NewRequest 的内容必须是实现 Reader 接口的类型，所以需要 strings.NewReader转化下。\nForm 表单提交的 content-type 要是 application/x-www-form-urlencoded，也要设置下。\n复杂的方式介绍完了。接着再介绍简化的方式，其实表单提交只需调用 http.PostForm 即可完成。示例代码如下：\npayload := make(url.Values) payload.Add(\u0026#34;name\u0026#34;, \u0026#34;poloxue\u0026#34;) payload.Add(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;) r, err := http.PostForm(\u0026#34;http://httpbin.org/post\u0026#34;, form) 竟是如此的简单。\n提交文件 # 文件提交应该是 HTTP 请求中较为复杂的内容了。其实说难也不难，区别于其他的请求，我们要花些精力来读取文件，组织提交POST的数据。\n举个例子，假设现在我有一个图片文件，名为 as.jpg，路径在 /Users/polo 目录下。现在要将这个图片提交给 http://httpbin.org/post。\n我们要先组织 POST 提交的内容，代码如下：\nfilename := \u0026#34;/Users/polo/as.jpg\u0026#34; f, err := os.Open(filename) if err != nil { panic(err) } defer func() { _ = f.Close() }() uploadBody := \u0026amp;bytes.Buffer{} writer := multipart.NewWriter(uploadBody) fWriter, err := writer.CreateFormFile(\u0026#34;uploadFile\u0026#34;, filename) if err != nil { fmt.Printf(\u0026#34;copy file writer %v\u0026#34;, err) } _, err = io.Copy(fWriter, f) if err != nil { panic(err) } fieldMap := map[string]string{ \u0026#34;filename\u0026#34;: filename, } for k, v := range fieldMap { _ = writer.WriteField(k, v) } err = writer.Close() if err != nil { panic(err) } 我认为，数据组织分为几步完成，如下：\n第一步，打开将要上传的文件，使用 defer f.Close() 做好资源释放的准备； 第二步，创建存储上传内容的 bytes.Buffer，变量名为 uploadBody； 第三步，通过 multipart.NewWriter 创建 writer，用于向 buffer中写入文件提供的内容； 第四步，通过writer.CreateFormFile 创建上传文件并通过 io.Copy 向其中写入内容； 最后，通过 writer.WriteField 添加其他的附加信息，注意最后要把 writer 关闭； 至此，文件上传的数据就组织完成了。接下来，只需调用 http.Post 方法即可完成文件上传。\nr, err := http.Post(\u0026#34;http://httpbin.org/post\u0026#34;, writer.FormDataContentType(), uploadBody) 有一点要注意，请求的content-type需要设置，而通过 writer.FormDataContentType() 即能获得上传文件的类型。\n到此，文件提交也完成了，不知道有没有非常简单的感觉。\nCookie # 主要涉及两部分内容，即读取响应的 cookie 与设置请求的 cookie。响应的 cookie 获取方式非常简单，直接调用 r.Cookies 即可。\n重点来说说，如何设置请求 cookie。cookie设置有两种方式，一种设置在 Client 上，另一种是设置在 Request 上。\nClient 上设置 Cookie # 直接看示例代码：\ncookies := make([]*http.Cookie, 0) cookies = append(cookies, \u0026amp;http.Cookie{ Name: \u0026#34;name\u0026#34;, Value: \u0026#34;poloxue\u0026#34;, Domain: \u0026#34;httpbin.org\u0026#34;, Path: \u0026#34;/cookies\u0026#34;, }) cookies = append(cookies, \u0026amp;http.Cookie{ Name: \u0026#34;id\u0026#34;, Value: \u0026#34;10000\u0026#34;, Domain: \u0026#34;httpbin.org\u0026#34;, Path: \u0026#34;/elsewhere\u0026#34;, }) url, err := url.Parse(\u0026#34;http://httpbin.org/cookies\u0026#34;) if err != nil { panic(err) } jar, err := cookiejar.New(nil) if err != nil { panic(err) } jar.SetCookies(url, cookies) client := http.Client{Jar: jar} r, err := client.Get(\u0026#34;http://httpbin.org/cookies\u0026#34;) 代码中，我们首先创建了 http.Cookie 切片，然后向其中添加了 2 个 Cookie 数据。这里通过 cookiejar，保存了 2 个新建的 cookie。\n这次我们不能再使用默认的 DefaultClient 了，而是要创建新的 Client，并将保存 cookie 信息的 cookiejar 与 client 绑定。接下里，只需要使用新创建的 Client 发起请求即可。\n请求上设置 Cookie # 请求上的 cookie 设置，通过 req.AddCookie即可实现。示例代码：\nreq, err := http.NewRequest(http.MethodGet, \u0026#34;http://httpbin.org/cookies\u0026#34;, nil) if err != nil { panic(err) } req.AddCookie(\u0026amp;http.Cookie{ Name: \u0026#34;name\u0026#34;, Value: \u0026#34;poloxue\u0026#34;, Domain: \u0026#34;httpbin.org\u0026#34;, Path: \u0026#34;/cookies\u0026#34;, }) r, err := http.DefaultClient.Do(req) 挺简单的，没什么要介绍的。\ncookie 设置 Client 和 设置在 Request 上有何区别？一个最易想到的区别就是，Request 的 cookie 只是当次请求失效，而 Client 上的 cookie 是随时有效的，只要你用的是这个新创建的 Client。\n重定向和请求历史 # 默认情况下，所有类型请求都会自动处理重定向。\nPython 的 requests 包中 HEAD 请求是不重定向的，但测试结果显示 net/http 的 HEAD 是自动重定向的。\nnet/http 中的重定向控制可以通过 Client 中的一个名为 CheckRedirect 的成员控制，它是函数类型。定义如下：\ntype Client struct { ... CheckRedirect func(req *Request, via []*Request) error ... } 接下来，我们来看看怎么使用。\n假设我们要实现的功能：为防止发生循环重定向，重定向次数定义不能超过 10 次，而且要记录历史 Response。\n示例代码：\nvar r *http.Response history := make([]*http.Response, 0) client := http.Client{ CheckRedirect: func(req *http.Request, hrs []*http.Request) error { if len(hrs) \u0026gt;= 10 { return errors.New(\u0026#34;redirect to many times\u0026#34;) } history = append(history, req.Response) return nil }, } r, err := client.Get(\u0026#34;http://github.com\u0026#34;) 首先创建了 http.Response 切片的变量，名称为 history。接着在 http.Client 中为 CheckRedirect 赋予一个匿名函数，用于控制重定向的行为。CheckRedirect 函数的第一个参数表示下次将要请求的 Request，第二个参数表示已经请求过的 Request。\n当发生重定向时，当前的 Request 会保存上次请求的 Response，故而此处可以将 req.Response 追加到 history 变量中。\n超时设置 # Request 发出后，如果服务端迟迟没有响应，那岂不是很尴尬。那么我们就会想，能否为请求设置超时规则呢？毫无疑问，当然可以。\n超时可以分为连接超时和响应读取超时，这些都可以设置。但正常情况下，并不想有那么明确的区别，那么也可以设置个总超时。\n总超时 # 总的超时时间的设置是绑定在 Client 的一个名为 Timeout 的成员之上，Timeout 是 time.Duration。\n假设这是超时时间为 10 秒，示例代码：\nclient := http.Client{ Timeout: time.Duration(10 * time.Second), } 连接超时 # 连接超时可通过 Client 中的 Transport 实现。Transport 中有个名为 Dial 的成员函数，可用设置连接超时。Transport 是 HTTP 底层的数据运输者。\n假设设置连接超时时间为 2 秒，示例代码：\nt := \u0026amp;http.Transport{ Dial: func(network, addr string) (net.Conn, error) { timeout := time.Duration(2 * time.Second) return net.DialTimeout(network, addr, timeout) }, } 在 Dial 的函数中，我们通过 net.DialTimeout 进行网络连接，实现了连接超时功能。\n读取超时 # 读取超时也要通过 Client 的 Transport 设置，比如设置响应的读取为 8 秒。\n示例代码：\nt := \u0026amp;http.Transport{ ResponseHeaderTimeout: time.Second * 8, } 综合所有，Client 的创建代码如下： t := \u0026amp;http.Transport{ Dial: func(network, addr string) (net.Conn, error) { timeout := time.Duration(2 * time.Second) return net.DialTimeout(network, addr, timeout) }, ResponseHeaderTimeout: time.Second * 8, } client := http.Client{ Transport: t, Timeout: time.Duration(10 * time.Second), } 除了上面的几个超时设置，Transport 还有其他一些关于超时的设置，可以看下 Transport 的定义，还有发现三个与超时相关的定义：\n// IdleConnTimeout is the maximum amount of time an idle // (keep-alive) connection will remain idle before closing // itself. // Zero means no limit. IdleConnTimeout time.Duration // ResponseHeaderTimeout, if non-zero, specifies the amount of // time to wait for a server\u0026#39;s response headers after fully // writing the request (including its body, if any). This // time does not include the time to read the response body. ResponseHeaderTimeout time.Duration // ExpectContinueTimeout, if non-zero, specifies the amount of // time to wait for a server\u0026#39;s first response headers after fully // writing the request headers if the request has an // \u0026#34;Expect: 100-continue\u0026#34; header. Zero means no timeout and // causes the body to be sent immediately, without // waiting for the server to approve. // This time does not include the time to send the request header. ExpectContinueTimeout time.Duration 分别是 IdleConnTimeout （连接空闲超时时间，keep-live 开启）、TLSHandshakeTimeout （TLS 握手时间）和 ExpectContinueTimeout（似乎已含在 ResponseHeaderTimeout 中了，看注释）。\n到此，完成了超时的设置。相对于 Python requests 确实是复杂很多。\n请求代理 # 代理还是挺重要的，特别对于开发爬虫的同学。那 net/http 怎么设置代理？这个工作还是要依赖 Client 的成员 Transport 实现，这个 Transport 还是挺重要的。\nTransport 有个名为 Proxy 的成员，具体看看怎么使用吧。假设我们要通过设置代理来请求谷歌的主页，代理地址为 http://127.0.0.1:8087。\n示例代码：\nproxyUrl, err := url.Parse(\u0026#34;http://127.0.0.1:8087\u0026#34;) if err != nil { panic(err) } t := \u0026amp;http.Transport{ Proxy: http.ProxyURL(proxyUrl), TLSClientConfig: \u0026amp;tls.Config{InsecureSkipVerify: true}, } client := http.Client{ Transport: t, Timeout: time.Duration(10 * time.Second), } r, err := client.Get(\u0026#34;https://google.com\u0026#34;) 主要关注 http.Transport 创建的代码。两个参数，分时 Proxy 和 TLSClientConfig，分别用于设置代理和禁用 https 验证。我发现其实不设置 TLSClientConfig 也可以请求成功，具体原因没仔细研究。\n错误处理 # 错误处理其实都不用怎么介绍，GO中的一般错误主要是检查返回的error，HTTP 请求也是如此，它会视情况返回相应错误信息，比如超时、网络连接失败等。\n示例代码中的错误都是通过 panic 抛出去的，真实的项目肯定不是这样的，我们需要记录相关日志，时刻做好错误恢复工作。\n总结 # 本文以 Python 的 requests 文档为指导方向，整理了 requests 快速入门文档中的案例在 GO 的是如何实现的。要说明的是， GO 其实也提供了对应于 requests 的克隆版本，github地址。暂时我也还没有看，有兴趣的朋友可以去研究一下。\n博文地址：Go 的 Http 请求系统指南\n","date":"2019-09-10","externalUrl":null,"permalink":"/posts/2019-09-10-the-guide-for-go-http-client/","section":"文章","summary":"前几天在 “知乎想法” 谈到了一个话题，如何模仿学习，举了通过 net/http client 模仿 Pyhton 的requests的例子。但并未实践，难道想法真的只能是想法吗？当然不是，于是我决定先暂停一周 GO 笔记，来实践下自己的想法。\n","title":"Go 的 Http 请求系统指南","type":"posts"},{"content":"第三篇 Go 问答总结，2019 年 8 月份总结，大约有 12 篇问答。前两遍地址如下：\nGo 问答汇总 Part One Go 问答汇总 Part Two\n问题大部分是来自于知乎和 segmentfault。本月有一个问题来自 stackoverflow，我的英文水平一般，读与翻译还行，但写起来还需要锻炼。虽然这一个回答没得到一个赞同，但能被题主采纳，我还是很荣幸的。\n最近发现，我的回答经常会被 Go 语言中文网的周刊收录。对 Go 感兴趣的朋友可以关注下 Go 语言中文网的公众号，内容还是非常丰富的，每天都会推送关于 Go 的优秀文章。\n开始正文！\ndynamodbattribute.UnmarshalMap canges the type of my variable to map[string]interface{}\n将 stackoverflow 的这篇回答放在首位吧，问题不是很难，重在第一次尝试，stackoverflow 上面有价值的问题还是很多的。\n题主的目标是希望 map 类型转化成 struct 类型。将问题稍微简化下，题主希望通过类似如下这种写法将 map[string]interface{} 转化为 user 类型。\npackage main import \u0026#34;fmt\u0026#34; type User struct { Name string } func Item() interface{} { return User{} } func ItemMap(s map[string]interface{}, item *interface{}) { *item = s } func main() { m := Item() fmt.Printf(\u0026#34;%T\\n\u0026#34;, m) fmt.Printf(\u0026#34;%v\\n\u0026#34;, m) s := map[string]interface{}{ \u0026#34;Name\u0026#34;: \u0026#34;poloxue\u0026#34;, } ItemMap(s, \u0026amp;m) fmt.Printf(\u0026#34;%T\\n\u0026#34;, m) fmt.Printf(\u0026#34;%v\\n\u0026#34;, m) } 首先，通过 Item() 方法创建一个 User 类型变量，但返回的类型是 interface{}，因而 main 主函数中的 m 通过类型推导，也就是 interface{} 类型，在 ItemMap 函数中将 map[string]interface{} 变量赋值给 m 的地址并不会有任何问题，因为虽然此时 m 类型的底层类型是 User，但赋值时并不会验查到这一层，经过一部分之后，m 的底层类型就由 User 转化成了 map[string]interface，并非完成了 map[string]interface{} 到 struct 的转化。\n看样子，map 和 struct 的转化还是很常见的需求啊，上月的问答总结就有类似的问题。再次推荐 mapstructure 包。\ngo源码的raceenable是做什么的，可否解答一二！\nGo 是内置并发支持的语言，并发的常见问题就是出现竞态条件，Go 专门提供了一个工具用于检测这个问题，使用非常方便，只需要在执行程序时加上 -race 选项，Go 源码中的这个 raceenable 表示的意思即使是否启用了 raceenable。\n前段时间翻译了一篇关于 go race 的文章，访问 Go 译文之竞态检测器 race。\nGolang中，runtime.Caller(skip)，为什么会保留编译器变量？\n题主的目标是获取 Go 可执行文件的当前路径，但对于很多习惯了使用解释型语言的朋友，养成了源码文件路径即可执行文件路径的思维，而 runtine.Caller 可用于查看调用栈信息，正好可以保留编译时的源码文件信息。\n但真实的项目部署的肯定是可执行文件，而非源码文件，而路径也是针对可执行文件的路径。如何获取到呢？请看回答吧！\n再提醒一点，正因为很多朋友养成了解释型语言的思维，常会通过 go run 执行源码，然后获取可执行文件路径，但最终得到的确实一个看不懂的路径。这个问题在回答也有介绍。\ngolang如何复用mysql连接？\ngolang 中的标准库 database/sql 已经提供了一份数据库管理的公共实现，同样涉及 mysql 连接复用的功能。\n题主的问题是为什么没有达到复用的效果。\n连接复用有一个重要前提就是资源使用结束后要记得释放，否则将会一直处于占用状态，至于有哪些情况会导致连接一直复用，查看回答！\n回答中提到了一篇非常系统介绍 Go 中 database/sql 使用的教程，有兴趣可以仔细读读。\ngoroutine 出现异常\n这个问题稍微有点复杂。异常的原因，简单说来是 channel 发送者发送等待时间太长，导致异常出现。解析问题的核心思路在于，在发送者和接收之间增加一个 buffer，无论发送者发送了什么内容，先将消息接收到 buffer 中，待有空闲的接收者时将消息从 buffer 中发送出去。\n题主是通过 chan 自带的 buffer 实现，创建了缓冲大小 50 的 chan。当然，更灵活的方式，还是自己实现 buffer 比较好。而且题主的场景是爬虫，可能对 buffer 的大小要求会比较高，可能适当考虑接入外部服务。\ngo的reflect是否可以获取到同一个package中，其他文件定义的类型的私有方法的名称？\n首先，要明确的是，Go 中的反射是无法看到类型的私有方法的。如果想达到这个目标，只能将方法定义为可导出方法。但私有方法也有它的好处，那就是外界无法使用这个方法。那将方法定义为公开可导出是否也可以做到让外界无法使用？\n查看我的回答吧！\nbeego 的缓存如何转结构体呢?\nbeego 中缓存的存储与获取的问题，很简单，没什么好说的！\ngo语言里的select监听到底是怎么工作的?\n问题的核心不在于 select 的工作机制，而是关于 Go 中 timer 定时器的使用问题。以下是问题的核心代码。\nfor { select { case num := \u0026lt;-ch: fmt.Println(\u0026#34;get num is \u0026#34;, num) case \u0026lt;-time.After(2 * time.Second): fmt.Println(\u0026#34;time\u0026#39;s up!!!\u0026#34;) //quit \u0026lt;- true } } }() 题主的问题是为什么 timer 的 case 一直没有执行。这是因为 time.After 在每次循环都会创建的 chan，之前定时 chan 已经不再监听了。\n我开始觉得，这种方式挺适合处理任务出现 timeout 的情况，经过一位朋友提醒，这里面存在着泄露风险。具体原因，阅读回答了解吧！\n在golang的设计里，为什么不能用switch实现select的功能？\n主要是场景决定吧，channel 监听有点类似于异步 IO 的多路复用，select 可以说是多路复用的专用名字了，switch 是分支结构，硬要加上其他功能，也不便于理解，我的感觉是，独立出一个新的关键字会更好理解。\ngo语言struct中有函数指数的示例讲解\n题主要求用从浅入深的方式介绍，Go 中 struct 含有函数成员的问题。我觉得我也写得还不多，但是没有得到题主的采纳，只得到一个评论夸赞。\ngolang 编译器分32和64版本吗？\n肯定是分的，但我们不用关心，go 命令已经封装好了，它会依据平台选择不同的底层命令。\n在 go 安装目录的 pkg 目录下有个 tools 目录，里面包含了编译链接时实际使用的命令，比如我的 Mac Pro，在 pkg/tool/darwin_amd64/ 下能找到 go 编译链接实际调用的命令 compile 和 link。darwin_amd64 中 drawin 表示操作系统，amd64 就是系统架构。\n执行 go env 会发现其中有个环境变量 GOTOOLDIR，它是那些与我们平台相关工具的所在目录。我的 Mac 下得到的结果是：\nGOTOOLDIR=\u0026#34;/usr/local/go/pkg/tool/darwin_amd64\u0026#34; 正则如何匹配最后一个页码?\n题主的问题是如何进行 html 的解析。\n\u0026lt;ul id=\u0026#34;pages\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;xxx1\u0026#34;\u0026gt;1\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;xxx2\u0026#34;\u0026gt;2\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;xxx3\u0026#34;\u0026gt;3\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; 这类问题一般是不推荐使用正则匹配，可以试试 goquery 这个库，它的用法类似 jquery，可用于解析 html 内容，使用非常方便。当然，回答中也介绍了正则的实现，不过代码的可读性看起来比较差。\n本月的回答总结完毕，如果大家发现有不当的内容，欢迎批评指正，非常感谢。\n博文地址：Go 问答汇总 Part Three\n","date":"2019-09-10","externalUrl":null,"permalink":"/posts/2019-09-10-zhihu-golang-part3/","section":"文章","summary":"第三篇 Go 问答总结，2019 年 8 月份总结，大约有 12 篇问答。前两遍地址如下：\nGo 问答汇总 Part One Go 问答汇总 Part Two\n问题大部分是来自于知乎和 segmentfault。本月有一个问题来自 stackoverflow，我的英文水平一般，读与翻译还行，但写起来还需要锻炼。虽然这一个回答没得到一个赞同，但能被题主采纳，我还是很荣幸的。\n","title":"Go 问答汇总 Part Three","type":"posts"},{"content":" 译者前言 # 第三篇 Go 官方博客译文，主要是关于 Go 内置的竞态条件检测工具。它可以有效地帮助我们检测并发程序的正确性。使用非常简单，只需在 go 命令加上 -race 选项即可。\n本文最后介绍了两个真实场景下的竞态案例，第一个案例相对比较简单。重点在于第二个案例，这个案例比较难以理解，在原文的基础上，我也简单做了些补充，不知道是否把问题讲的足够清楚。同时，这个案例也告诉我们，任何时候我们都需要重视检测器给我们的提示，因为一不小心，你就可能为自己留下一个大坑。\n概要 # 在程序世界中，竞态条件是一种潜伏深且很难发现的错误，如果将这样的代码部署线上，常会产生各种谜一般的结果。Go 对并发的支持让我们能非常简单就写出支持并发的代码，但它并不能阻止竞态条件的发生。\n本文将会介绍一个工具帮助我们实现它。\nGo 1.1 加入了一个新的工具，竞态检测器，它可用于检测 Go 程序中的竞态条件。当前，运行在 x86_64 处理器的 Linux、Mac 或 Windows 下可用。\n竞态检测器的实现基于 C/C++ 的 ThreadSanitizer 运行时库，ThreadSanitier 在 Googgle 已经被用在一些内部基础库以及 Chromium上，并且帮助发现了很多有问题的代码。\nThreadSanitier 这项技术在 2012 年 9 月被集成到了 Go 上，它帮助检测出了标准库中的 42 个竞态问题。它现在已经是 Go 构建流程中的一部分，当竞态条件出现，将会被它捕获。\n如何工作 # 竞态检测器集成在 Go 工具链，当命令行设置了 -race 标志，编译器将会通过代码记录所有的内存访问，何时以及如何被访问，运行时库也会负责监视共享变量的非同步访问。当检测到竞态行为，警告信息会把打印出来。（具体详情阅读 文章）\n这样的设计导致竞态检测只能在运行时触发，这也意味着，真实环境下运行 race-enabled 的程序就变得非常重要，但 race-enabled 程序耗费的 CPU 和内存通常是正常程序的十倍，在真实环境下一直启用竞态检测是非常不切合实际的。\n是否感受到了一阵凉凉的气息？\n这里有几个解决方案可以尝试。比如，我们可以在 race-enabled 的情况下执行测试，负载测试和集成测试是个不错的选择，它偏向于检测代码中可能存在的并发问题。另一种方式，可以利用生产环境的负载均衡，选择一台服务部署启动竞态检测的程序。\n开始使用 # 竞态检测器已经集成到 Go 工具链中了，只要设置 -race 标志即可启用。命令行示例如下：\n$ go test -race mypkg $ go run -race mysrc.go $ go build -race mycmd $ go install -race mypkg 通过具体案例体验下，安装运行一个命令，步骤如下：\n$ go get -race golang.org/x/blog/support/racy $ racy 接下来，我们介绍 2 个实际的案例。\n案例 1：Timer.Reset # 这是一个由竞态检测器发现的真实的 bug，这里将演示的是它的一个简化版本。我们通过 timer 实现随机间隔（0-1 秒）的消息打印，timer 会重复执行 5 秒。\n首先，通过 time.AfterFunc 创建 timer，定时的间隔从 randomDuration 函数获得，定时函数打印消息，然后通过 timer 的 Reset 方法重置定时器，重复利用。\nfunc main() { start := time.Now() var t *time.Timer t = time.AfterFunc(randomDuration(), func() { fmt.Println(time.Now().Sub(start)) t.Reset(randomDuration()) }) time.Sleep(5 * time.Second) } func randomDuration() time.Duration { return time.Duration(rand.Int63n(1e9)) } 我们的代码看起来一切正常。但在多次运行后，我们会发现在某些特定情况下可能会出现如下错误：\nanic: runtime error: invalid memory address or nil pointer dereference [signal 0xb code=0x1 addr=0x8 pc=0x41e38a] goroutine 4 [running]: time.stopTimer(0x8, 0x12fe6b35d9472d96) src/pkg/runtime/ztime_linux_amd64.c:35 +0x25 time.(*Timer).Reset(0x0, 0x4e5904f, 0x1) src/pkg/time/sleep.go:81 +0x42 main.func·001() race.go:14 +0xe3 created by time.goFunc src/pkg/time/sleep.go:122 +0x48 什么原因？启用下竞态检测器测试下吧，你会恍然大悟的。\n$ go run -race main.go ================== WARNING: DATA RACE Read by goroutine 5: main.func·001() race.go:14 +0x169 Previous write by goroutine 1: main.main() race.go:15 +0x174 Goroutine 5 (running) created at: time.goFunc() src/pkg/time/sleep.go:122 +0x56 timerproc() src/pkg/runtime/ztime_linux_amd64.c:181 +0x189 ================== 结果显示，程序中存在 2 个 goroutine 非同步读写变量 t。如果初始定时时间非常短，就可能出现在主函数还未对 t 赋值，定时函数已经执行，而此时 t 仍然是 nil，无法调用 Reset 方法。\n我们只要把变量 t 的读写移到主 goroutine 执行，就可以解决问题了。如下：\nfunc main() { start := time.Now() reset := make(chan bool) var t *time.Timer t = time.AfterFunc(randomDuration(), func() { fmt.Println(time.Now().Sub(start)) reset \u0026lt;- true }) for time.Since(start) \u0026lt; 5*time.Second { \u0026lt;-reset t.Reset(randomDuration()) } } main goroutine 完全负责 timer 的初始化和重置，重置信号通过一个 channel 负责传递。\n当然，这个问题还有个更简单直接的解决方案，避免重用定时器即可。示例代码如下：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;time\u0026#34; ) func main() { start := time.Now() var f func() f = func() { fmt.Println(time.Now().Sub(start)) time.AfterFunc(time.Duration(rand.Int63n(1e9)), f) } time.AfterFunc(time.Duration(rand.Int63n(1e9)), f) time.Sleep(5 * time.Second) } 代码非常简洁易懂，缺点呢，就是效率相对不高。\n案例 2：ioutil.Discard # 这个案例的问题隐藏更深。\nioutil 包中的 Discard 实现了 io.Writer 接口，不过它会丢弃所有写入它的数据，可类比 /dev/null。可在我们需要读取数据但又不准备保存的场景下使用。它常常会和 io.Copy 结合使用，实现抽空一个 reader，如下：\nio.Copy(ioutil.Discard, reader) 时间回溯至 2011 年，当时 Go 团队注意以这种方式使用 Discard 效率不高，Copy 函数每次调用都会在内部分配 32 KB 的缓存 buffer，但我们只是要丢弃读取的数据，并不需要分配额外的 buffer。我们认为，这种习惯性的用法不应该这样耗费资源。\n解决方案非常简单，如果指定的 Writer 实现了 ReadFrom 方法，io.Copy(writer, reader) 调用内部将会把读取工作委托给 writer.ReadFrom(reader) 执行。\nDiscard 类型增加 ReadFrom 方法共享一个 buffer。到这里，我们自然会想到，这里理论上会存在竞态条件，但因为写入到 buffer 中的数据会被立刻丢弃，我们就没有太重视。\n竞态检测器完成后，这段代码立刻被标记为竞态的，查看 issues/3970。这促使我们再一次思考，这段代码是否真的存在问题呢，但结论依然是这里的竞态不影响程序运行。为了避免这种 \u0026ldquo;假的警告\u0026rdquo;，我们实现了 2 个版本的 black_hole buffer，竞态版本和无竞态版本。而无竞态版只会其在启用竞态检测器的时候启用。\nblack_hole.go，无竞态版本。\n// +build race package ioutil // Replaces the normal fast implementation with slower but formally correct one. func blackHole() []byte { return make([]byte, 8192) } black_hole_race.go，竞态版本。\n// +build !race package ioutil var blackHoleBuf = make([]byte, 8192) func blackHole() []byte { return blackHoleBuf } 但几个月后，Brad 遇到了一个迷之 bug。经过几天调试，终于确定了原因所在，这是一个由 ioutil.Discard 导致的竞态问题。\n实际代码如下：\nvar blackHole [4096]byte // shared buffer func (devNull) ReadFrom(r io.Reader) (n int64, err error) { readSize := 0 for { readSize, err = r.Read(blackHole[:]) n += int64(readSize) if err != nil { if err == io.EOF { return n, nil } return } } } Brad 的程序中有一个 trackDigestReader 类型，它包含了一个 io.Reader 类型字段，和 io.Reader 中信息的 hash 摘要。\ntype trackDigestReader struct { r io.Reader h hash.Hash } func (t trackDigestReader) Read(p []byte) (n int, err error) { n, err = t.r.Read(p) t.h.Write(p[:n]) return } 举个例子，计算某个文件的 SHA-1 HASH。\ntdr := trackDigestReader{r: file, h: sha1.New()} io.Copy(writer, tdr) fmt.Printf(\u0026#34;File hash: %x\u0026#34;, tdr.h.Sum(nil)) 某些情况下，如果没有地方可供数据写入，但我们还是需要计算 hash，就可以用 Discard 了。\nio.Copy(ioutil.Discard, tdr) 此时的 blackHole buffer 并非仅仅是一个黑洞，它同时也是 io.Reader 和 hash.Hash 之间传递数据的纽带。当多个 goroutine 并发执行文件 hash 时，它们全部共享一个 buffer，Read 和 Write 之间的数据就可能产生相应的冲突。No error 并且 No panic，但是 hash 的结果是错的。就是如此可恶。\nfunc (t trackDigestReader) Read(p []byte) (n int, err error) { // the buffer p is blackHole n, err = t.r.Read(p) // p may be corrupted by another goroutine here, // between the Read above and the Write below t.h.Write(p[:n]) return } 最终，通过为每一个 io.Discard 提供唯一的 buffer，我们解决了这个 bug，排除了共享 buffer 的竞态条件。代码如下：\nvar blackHoleBuf = make(chan []byte, 1) func blackHole() []byte { select { case b := \u0026lt;-blackHoleBuf: return b default: } return make([]byte, 8192) } func blackHolePut(p []byte) { select { case blackHoleBuf \u0026lt;- p: default: } } iouitl.go 中的 devNull ReadFrom 方法也做了相应修正。\nfunc (devNull) ReadFrom(r io.Reader) (n int64, err error) { buf := blackHole() defer blackHolePut(buf) readSize := 0 for { readSize, err = r.Read(buf) // other } 通过 defer 将使用完的 buffer 重新发送至 blackHoleBuf，因为 channel 的 size 为 1，只能复用一个 buffer。而且通过 select 语句，我们在没有可用 buffer 的情况下，创建新的 buffer。\n结论 # 竞态检测器，一个非常强大的工具，在并发程序的正确性检测方面有着很重要的地位。它不会发出假的提示，认真严肃地对待它的每条警示非常必要。但它并非万能，还是需要以你对并发特性的正确理解为前提，才能真正地发挥出它的价值。\n试试吧！开始你的 go test -race。\n我的博文：Go 的静态检测功能，译：Introducing the Go Race Detector\n","date":"2019-09-01","externalUrl":null,"permalink":"/posts/2019-09-01-golang-race/","section":"文章","summary":"译者前言 # 第三篇 Go 官方博客译文，主要是关于 Go 内置的竞态条件检测工具。它可以有效地帮助我们检测并发程序的正确性。使用非常简单，只需在 go 命令加上 -race 选项即可。\n","title":"Go 的静态检测功能","type":"posts"},{"content":"不论是开源项目，还是日常程序的开发，测试都是必不可少的一个环节。今天我们开始进入 Go 测试模块 testing 的介绍。\n简单概述 # 我们选择开源项目，通常会比较关注这个项目的测试用例编写的是否完善，一个优秀项目的测试一般写的不会差。为了日后自己能写出一个好的项目，测试这块还是要好好学习下。\n常接触的测试主要是单元测试和性能测试。毫无意外，go 的 testing 也支持这两种测试。单元测试用于模块测试，而性能则是由基准测试完成，即 benchmark。\nGo 测试模块除了上面提到的功能，还有一项能力，支持编写案例，通过与 godoc 的结合，可以非常快捷地生成库文档。\n最易想到的方法 # 谈到如何测试一个函数的功能，对开发来说，最容易想到的方法就是在 main 中直接调用函数判断结果。\n举个例子，测试 math 方法下的绝对值函数 Abs，示例代码如下：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;math\u0026#34; ) func main() { v := math.Abs(-10) if v != 10 { fmt.Println(\u0026#34;测试失败\u0026#34;) return } fmt.Println(\u0026#34;测试成功\u0026#34;) } 更常见的可能是，if 判断都没有，直接 Print 输出结果，我们观察结果确认问题。特别对于习惯使用 Python、PHP 脚本语言的开发， 建一个脚本测试是非常快速的，因为曾经很长一段时间，我就是如此。\n这种方式有什么缺点？我的理解，主要几点，如main 中的测试不容易复用，常常是建了就删；测试用例变多时，灵活性不够，常会有修改代码的需求；自动化测试也不是非常方便等等问题。\n我对测试的了解不是很深，上面这些仅仅我的一些体验吧。\n遇到了问题就得解决，下面正式开始进入 go testing 中单元测试的介绍。\n一个快速体验案例 # 单元测试用于在指定场景下，测试功能模块在指定的输入情况下，确定有没有按期望结果输出结果。\n我们直接看个例子，简单直观。测试 math 下的 Abs 绝对值函数。首先，在某个目录创建测试文件 math_test.go，代码如下：\npackage math import ( \u0026#34;math\u0026#34; \u0026#34;testing\u0026#34; ) func TestAbs(t *testing.T) { var a, expect float64 = -10, 10 actual := math.Abs(a) if actual != expect { t.Fatalf(\u0026#34;a = %f, actual = %f, expected = %f\u0026#34;, a, actual, expect) } } 程序非常简洁，a 是 Abs 函数的输入参数，expect 是期望得到的执行结果，actual 是函数执行的实际结果，测试结果由 actual 和 expect 比较结果确定。\n完成用例编写，go test 命令执行测试，我们会看到如下输出。\n$ go test PASS ok study/test/math 0.004s 输出为 PASS，表示测试用例成功执行。0.004s 表示用例执行时间。\n学会使用 go testing # 从前面例子中可以了解到，Go 的测试写起来还是非常方便的。关于它的使用方式，主要有两点，一是测试代码的编写规则，二是 API 的使用。\n测试的编写规则 # Go 的测试必须按规则方式编写，不然 go test 将无法正确定位测试代码的位置，主要三点规则。\n首先，测试代码文件的命名必须是以 _test.go 结尾，比如上节中的文件名 math_tesh.go 并非随意取的。\n还有，代码中的用例函数必须满足匹配 TestXxx，比如 TestAbs。\n关于 Xxx，简单解释一下，它主要传达两点含义，一是 Xxx 表示首个字符必须大写或数字，简单而言就是可确定单词分隔，二是首字母后的字符可以是任意 Go 关键词合法字符，如大小写字母、下划线、数字。\n第三，关于用例函数类型定义，定义如下。\nfunc TestXxx(*testing.T) 测试函数必须按这个固定格式编写，否则 go test 将执行报错。函数中有一个输入参数 t, 类型是 *testing.T，它非常重要，单元测试需通过它反馈测试结果，具体后面再介绍。\n灵活记忆 API 的使用 # 按规则编写测试用例只能保证 go test 的正确定位执行。但为了可以分析测试结果，我们还需要与测试框架进行交互，这就需要测试函数输入参数 t 的参与了。\n在 TestAbs 中，我们用到了 t.Fatalf，它的作用就是反馈测试结果。假设没有这段代码，发生错误也会反馈测试成功，这显然不是我们想要的。\n我们可以通过官方文档，看下 testing.T 中支持的可导出方法，如下：\n// 获取测试名称 method (*T) Name() string // 打印日志 method (*T) Log(args ...interface{}) // 打印日志，支持 Printf 格式化打印 method (*T) Logf(format string, args ...interface{}) // 反馈测试失败，但不退出测试，继续执行 method (*T) Fail() // 反馈测试成功，立刻退出测试 method (*T) FailNow() // 反馈测试失败，打印错误 method (*T) Error(args ...interface{}) // 反馈测试失败，打印错误，支持 Printf 的格式化规则 method (*T) Errorf(format string, args ...interface{}) // 检测是否已经发生过错误 method (*T) Failed() bool // 相当于 Error + FailNow，表示这是非常严重的错误，打印信息结束需立刻退出。 method (*T) Fatal(args ...interface{}) // 相当于 Errorf + FailNow，与 Fatal 类似，区别在于支持 Printf 格式化打印信息； method (*T) Fatalf(format string, args ...interface{}) // 跳出测试，从调用 SkipNow 退出，如果之前有错误依然提示测试报错 method (*T) SkipNow() // 相当于 Log 和 SkipNow 的组合 method (*T) Skip(args ...interface{}) // 与Skip，相当于 Logf 和 SkipNow 的组合，区别在于支持 Printf 格式化打印 method (*T) Skipf(format string, args ...interface{}) // 用于标记调用函数为 helper 函数，打印文件信息或日志，不会追溯该函数。 method (*T) Helper() // 标记测试函数可并行执行，这个并行执行仅仅指的是与其他测试函数并行，相同测试不会并行。 method (*T) Parallel() // 可用于执行子测试 method (*T) Run(name string, f func(t *T)) bool 上面列出了单元测试 testing.T 中所有的公开方法，我个人思路，把它们大概分为三类，分别是底层方法、测试反馈，还有一些其他运行控制的辅助方法。\n基础信息的 API 只有 1 个，Name() 方法，用于获取测试名称。运行控制的辅助方法主要指的是 Helper、t.Parallel 和 Run，上面的注释对它们已经做了简单介绍。\n我们这里重点说说测试反馈的 API，毕竟它用的最多。前面用到的 Fatalf 方法就是其中之一，它的效果是打印错误日志并立刻退出测试。希望速记这类 API 吗？我们或许可以按几个层级进行记忆。\n首先，我们记住一些相关的基础方法，它们是其它方法的核心组成，如下：\n日志打印，Log 与 Logf，Log 和 Logf 区别可对比 Println 和 Printf，即 Logf 支持 Printf 格式化打印，而 Log 不支持。 失败标记，Fail 和 FailNow，Fail 与 FailNow 都是用于标记测试失败的方法，它们的区别在于 Fail 标记失败后还会继续执行执行接下来的测试，而 FailNow 在标记失败后会立刻退出。 测试忽略，SkipNow 方法退出测试，但并不会标记测试失败，可与 FailNow 对比记忆。 我们再看看剩余的那些方法，基本都是由基础方法组合而来。我们可根据场景，选择不同的组合。比如：\n普通日志，只是打印一些日志，可以直接使用 Log 或 Logf 即可； 普通错误，如果不退出测试，只是打印一些错误提示信息，使用 Error 或 Errorf，这两个方法是 log 或 logf 和 Fail 的组合； 严重错误，需要退出测试，并打印一些错误提示信息，使用 Fatal (log + FailNow) 或 Fatalf (logf + FailNow)； 忽略错误，并退出测试，可以使用 Skip (log + SkipNow) 和 Skipf (logf + SkipNow)； 如果支持 Printf 的格式化信息打印，方法后面都会有一个 f 字符。如此一总结，我们发现 testing.T 中的方法的记忆非常简单。\n突然想到，不知是否有人会问什么情况下算是测试成功。其实，只要没有标记失败，测试就是成功的。\n实践一个案例 # 讲了那么多基础知识，我都有点口感舌燥了。现在，开始尝试使用一下它吧！\n举一个简单的例子，测试一个除法函数。首先，创建一个 math.go 文件。函数代码如下：\npackage math import \u0026#34;errors\u0026#34; func Division(a, b float64) (float64, error) { if b == 0 { return 0, errors.New(\u0026#34;division by zero\u0026#34;) } return a / b, nil } Division 非常简单，输入参数 a、b 分别是被除数和除数，输出参数是计算结果和错误提示。如果除数是 0，将会给出相应的错误提示。\n在正式测试 Division 函数前，我们先要梳理下什么样的输入与期望结果表示测试成功。输入不同，期望结果也就不同，可能是正确结果，亦或者是期待的错误结果。什么意思？以这里的 Division 为例，两种场景需要考虑：\n正常调用返回结果，比如当被除数为 10，除数为 5，期望得到的结果为 2，即期望得到正确的结果； 期望错误返回结果，当被除数为 10，除数为 0，期望返回除数不能为 0 的错误，即期望返回错误提示； 如果是测试驱动开发，在我们正式写实现代码前，就需要把这些先定义好，并且写好测试代码。\n分析完用例就可以开始写代码啦。\n先是正常调用的测试，如下：\nfunc TestDivision(t *testing.T) { var a, b, expect float64 = 10, 5, 2 actual, err := Division(a, b) if err != nil { t.Errorf(\u0026#34;a = %f, b = %f, expect = %f, err %v\u0026#34;, a, b, expect, err) return } if actual != expect { t.Errorf(\u0026#34;a = %f, b = %f, expect = %f, actual = %f\u0026#34;, a, b, expect, actual) } } 定义了三个变量，分别是 a、b、expect，对应被除数、除数和期望结果。用例通过对比 Division 的实际结果 actual 与期望结果 expect 确认测试是否成功。还有就是，Division 返回的 error 也要检查，因为这里期待的正常运行结果，只要有错即可认定测试失败。\n再看期望错误结果，如下：\nfunc TestDivisionZero(t *testing.T) { var a, b float64 = 10, 0 var expectedErrString = \u0026#34;division by zero\u0026#34; _, err := Division(a, b) if err.Error() != expectedErrString { t.Errorf(\u0026#34;a = %f, b = %f, err %v, expect err %s\u0026#34;, a, b, err, expectedErrString) return } } 同样是首先定义了三个变量，a、b 和 expectErrString，a、b 含义与之前相同，expectErrString 为预期提示的错误信息。除数 b 设置为 0 ，主要是为了测试 Division 函数是否能按预期返回错误，所以我们并不关心计算结果。测试成功与否，通过比较实际的返回 error 与 expectErrString 确定。\n通过 go test 执行测试，如下：\n$ go test -v === RUN TestDivision --- PASS: TestDivision (0.00s) === RUN TestDivisionZero --- PASS: TestDivisionZero (0.00s) PASS ok study/test/math 0.005s 结果显示，测试成功！\n这个案例的演示中，我们在 go test 上加入 -v 选项，这样就可以清晰地看到每个测试用例的执行情况。\n简洁紧凑的表组测试 # 通过上面的例子，不知道有没有发现一个问题？\n如果将要测试的某个功能函数的用例非常多，我们将会需要写很多代码重复度非常高的测试函数，因为对于单元测试而言，基本都是围绕一个简单模式：\n指定输入参数 -\u0026gt; 调用要测试的函数 -\u0026gt; 获取返回结果 -\u0026gt; 比较实际返回与期望结果 -\u0026gt; 确认测试失败提示\n基于此，Go 提倡我们使用一种称为 \u0026ldquo;Table Driven\u0026rdquo; 的测试方式，中文翻译，可称为表组测试。它可以让我们以一种短小紧密的方式编写测试。具体如何做呢？\n首先，我们要定义一个用于表组测试的结构体，其中要包含测试所需的输入与期望的输出。以 Division 函数测试为例，可以定义如下的结构体：\ntype DivisionTable struct { a float64 // 被除数 b float64 // 除数 expect float64 // 期待计算值 expectErr error // 期待错误字符串 } 各字段的含义在注释部分已经做了相关说明，和我们之前做的单个场景的测试涉及字段差不多。区别在于 expectErr 不再是 string 类型。\n接下来，将下面我们需要测试的用例通过该结构体字面量表示出来。\nvar table = []DivisionTable{ {1., 1., 1., nil}, {-4., -2., 2., nil}, {2., 0., 7., errors.New(\u0026#34;division by zero\u0026#34;)}, } 简单列举了三种场景，分别正数之间的除法、负数之间的除数以及除数为 0 的情况下的除法。\n接下来的目标就是实现一个通用 Division 测试函数。直接看代码吧！\nfunc TestDivisionTable(t *testing.T) { for _, v := range divisionTable { actual, err := Division(v.a, v.b) if err == nil { if v.expectErr != nil { t.Errorf( \u0026#34;a = %f, b = %f, actual err not nil, expect err is nil\u0026#34;, v.a, v.b) } } else if err != nil { if v.expectErr == nil { t.Errorf( \u0026#34;a = %f, b = %f, actual err not nil, expect err is nil\u0026#34;, v.a, v.b) } else if !strings.Contains(err.Error(), v.expectErr.Error()) { t.Errorf( \u0026#34;a = %f, b = %f, actual err = %v, expect err = %v\u0026#34;, v.a, v.b, err, v.expectErr) } } else if actual != v.expect { t.Errorf( \u0026#34;a = %f, b = %f, actual = %f, expect = %f\u0026#34;, v.a, v.b, actual, v.expect) } } } 代码看起来比较乱，这主要是因为 error 接口内部实际类型是指针，不能直接使用比较操作符对比 error，所以要做一些处理。如果没有错误的比较，这个例子就容易理解的多了。\n可以梳理一下，逻辑其实非常简单。主要由几个步骤组成：\n首先遍历 divisionTable，获取到输入参数与期望结果； 使用从 divisionTable 获取到输入参数调用功能函数； 获取功能函数的执行结果，包括计算结果与可能的错误； 对比返回 err 与 expectErr，需要处理 err == nil 和 != nil 两种情况； 实际 err 为 nil， expectErr 也需要为 nil； 实际 err 不为 nil，expectErr 不可为 nil，且错误信息包含在 err 中。 最后一步，比较实际计算结果与期望结果； 如果发生错误，我们使用 t.Errorf 打印错误日志并告知测试失败。用 Errorf 的原因是我们不能只是一个用例失败就退出整个测试。当然，这个要视情况而定吧，没有固定规则。\n介绍到这，核心部分就讲的差不多了。\n详细的日志输出 # 对于一些刚接触 Go testing 的朋友，可能碰到过接下来要讲的这个奇怪的问题。\n例子说明最直观，如下：\nfunc TestDivision(t *testing.T) { var a, b, expect float64 = 10, 5, 2 actual, err := Division(a, b) if err != nil { t.Errorf(\u0026#34;a = %f, b = %f, expect = %f, err %v\u0026#34;, a, b, expect, err) return } if actual != expect { t.Errorf(\u0026#34;a = %f, b = %f, expect = %f, actual = %f\u0026#34;, a, b, expect, actual) } t.Log(\u0026#34;end\u0026#34;) } 还是之前的例子，相比之下，最后增加了一段日志打印 t.Log(\u0026ldquo;end\u0026rdquo;)。不加任何选项的 go test 的执行效果如下：\n$ go test PASS ok study/test/math 0.004s 输出日志中并没看到增加那行 end 日志。\n前面的演示中，我们用到了 go test 的 -v 选项，通过它，可以查看非常详细的输出信息。我们加上 -v 选项，再执行看效果：\n$ go test -v === RUN TestDivision --- PASS: TestDivision (0.00s) math_test.go:36: end PASS ok study/test/math 0.005s 多出了很多信息，并且打出了那行 end 日志，并给出代码的位置 math_test.go:36: end。除此以外，还具体到了每一个测试的执行情况，比如测试执行开始和测试结果。\n灵活控制运行哪些测试 # 假设，我们把前面演示用到的那些测试函数全部放在 math_test.go 中。此时，使用默认 go test 测试会遇到一个问题，那就是每次都将包中的测试函数都执行一遍。有什么办法能灵活控制呢？\n可以先来看看此类问题，常见的使用场景有哪些！我想到的几点，如下：\n执行 package 下所有测试函数，go test 默认就是如此，不用多说； 执行其中的某一个测试函数，比如当我们把前面写的所有测试函数都放在了 math_test.go 文件中，如何选择其中一个执行； 按某一类匹配规则执行测试函数，比如执行名称满足以 Division 开头的测试函数； 执行项目下的所有测试函数，一个项目通常不止一个包，如何要将所有包的测试函数都执行一遍，该如何做呢； 第一个本不怎么用介绍了。但有一点还是要介绍下，那就是除默认执行当前路径的包，我们也可以具体指定执行哪个 package 的测试函数，指定方式支持纯粹的文件路径方式以及包路径方式。\n假设，我们包的导入路径为 example/math，而我们当前位置在 example 目录下，就有两种方式执行 math 下的测试。\n$ go test # 目录路径执行 $ go test example/math # GOPATH 包导入路径 第二、三场景，执行其中的某个或某类测试，主要与 go test 的 -run 选项有关，-run 选项接收参数是正则表达式。\n执行某一个具体的函数，如 TestDivision，命令执行效果如下：\n$ go test -run \u0026#34;^TestDivision$\u0026#34; -v === RUN TestDivision --- PASS: TestDivision (0.00s) math_test.go:36: end PASS ok study/test/math 0.004s 从输出中可了解到，确实只执行了 TestDivision。这里要记住加上 -v 选项，使输出信息具体到某一个测试。\n执行具体的某一个类的函数，如除法相关测试 Division，命令执行效果如下：\n$ go test -run \u0026#34;Division\u0026#34; -v === RUN TestDivision --- PASS: TestDivision (0.00s) math_test.go:36: end === RUN TestDivisionZero --- PASS: TestDivisionZero (0.00s) === RUN TestDivisionTable --- PASS: TestDivisionTable (0.00s) PASS ok _/Users/polo/Public/Work/go/src/study/test/math\t0.005s 将前面写过的函数名中包含 Division 全部执行一遍。\n第四个场景，执行整个项目下的测试。在项目的顶层目录，直接执行 go test ./\u0026hellip; 即可，具体就不演示了。\n总结 # 本文主要介绍了 Go 中测试模块使用的一些基础方法。从最容易想到的通过 main 测试方法到使用 go testing 中编写单元测试，接着介绍了一个测试案例，由此引入了 Go 中推荐 \u0026ldquo;Table Driven\u0026rdquo; 的测试方式。最后，文章还介绍了一些对我们平时工作比较有实际意义的技巧。\n博文地址：如何测试你的 Go 代码\n参考 # Unit Testing make easy in Go\ngotests\ngo test 单元测试\ntesting - 单元测试\nWriting table driven tests in Go\nHow to write benchmarks in Go\nGo 如何写测试用例\n","date":"2019-08-22","externalUrl":null,"permalink":"/posts/2019-08-22-how-to-test-your-code-in-golang/","section":"文章","summary":"不论是开源项目，还是日常程序的开发，测试都是必不可少的一个环节。今天我们开始进入 Go 测试模块 testing 的介绍。\n简单概述 # 我们选择开源项目，通常会比较关注这个项目的测试用例编写的是否完善，一个优秀项目的测试一般写的不会差。为了日后自己能写出一个好的项目，测试这块还是要好好学习下。\n","title":"如何测试你的 Go 代码","type":"posts"},{"content":" 译者前言 # 这篇博文介绍的内容比较实在，主要是关于两方面的内容。一是介绍 reflection 在 encoding/json 中的应用，另一个开发了一个 Cacher 工厂函数，实现函数式编程中的记忆功能，其实就是根据输入对输出进行一定限期的缓存。\n这篇文章的翻译没有上一篇那么轻松，因为涉及了一些函数式编程的术语，之前也并没有接触过。为了翻译这篇文章，简单阅读了网上的一篇关于函数式编程的文章，文章地址。望没有知识性错误。\n译文如下：\n上一篇文章，(阅读英文原版)，我们介绍了 Go 的反射包 reflection。并通过一些示例介绍了它的特性。但是，我们还不清楚它究竟有什么。\n通过反射实现的功能，不用反射也能实现，而且更加高效简洁。但是 Go 团队肯定不会因为自己而为 Go 增加一个新的特性。\n那究竟什么情况下会使用反射呢？\n寻找反射使用案例 # 通过反射，我们可以实现各种奇淫巧技。但每天的工作中，我该如何使用它呢？\n其实，大部分时间里，我们都用不到它。反射主要是用在一些特殊的场景下，使一些不可能的实现成为可能。我们常会在一些库、工具、框架中找到反射的使用场景。\n那你是否可以告诉我，哪些库、框架或工具中使用反射呢？一个技巧，查看函数参数类型。如果一个函数的参数类型是 interface{}，那么，它极有可能使用了反射来检查或改变参数的值。\nJSON 处理 # 反射，最常见的使用场景之一，是对网络或文件中的数据进行解包和组包。当你通过 struct tag 映射 JSON 或数据库中的数据时，便是通过反射实现的。这类场景，我们通常会用某个库帮助我们创建结构体实例，它通过分析 struct tag 和数据，以此为 struct 的字段赋值。\n我们就以 Go 官方标准库中的 JSON 解包为例，来介绍一下它的实现。\n通过调用 json.Unmarshal 函数，我们可以把 JSON 字符串解包并赋值给某个变量。Unmarshal 函数接收两个参数：\n类型为 []byte 的 JSON 字符串； 类型为 interface{}，用于存放 JSON 解析结果的变量； 深入看看这个函数究竟是如何进行反射的？\n阅读 json 包的源码，其中有个私有函数 unmarshal，主要看其中与反射相关的部分代码如下：\nfunc (d *decodeState) unmarshal(v interface{}) (err error) { \u0026lt;skip some setup\u0026gt; rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr || rv.IsNil() { return \u0026amp;InvalidUnmarshalError{reflect.TypeOf(v)} } d.scan.reset() // We decode rv not rv.Elem because the Unmarshaler interface // test must be applied at the top level of the value. // 传的是 rv，而不是 rv.Elem，因为结果传递给最顶层的 value d.value(rv) return d.savedError } 上面的代码中，首先会通过反射验证变量类型，是否是指针类型，如果是，将变量 v 的 reflect.Value 传给 value 方法。\n在 value 方法中，首先检查 JSON 字符串表示的类型，array、object、还是字面量。不同的类型将由不同方法处理。举例来说，如果解析 JSON Object。\n将会有很多地方用到反射。\n比如，使用反射检查 v 是否是 nil interface。\n// Decoding into nil interface? Switch to non-reflect code. if v.Kind() == reflect.Interface \u0026amp;\u0026amp; v.NumMethod() == 0 { v.Set(reflect.ValueOf(d.objectInterface())) return } 如果是把 JSON object 赋值给 map。\nswitch v.Kind() { case reflect.Map: // Map key must either have string kind, have an integer kind, // or be an encoding.TextUnmarshaler. t := v.Type() switch t.Key().Kind() { case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: default: if !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) { d.saveError(\u0026amp;UnmarshalTypeError{Value: \u0026#34;object\u0026#34;, Type: v.Type(), Offset: int64(d.off)}) d.off -- d.next() // skip over { } in input return } } if v.IsNil() { v.Set(reflect.MakeMap(t)) } case reflect.Struct: // ok default: d.saveError(\u0026amp;UnmarshalTypeError{Value: \u0026#34;object\u0026#34;, Type: v.Type(), Offset: int64(d.off)}) d.off -- d.next() // skip over { } in input return } 如果是把 JSON object 赋值给 struct。\nsubv = v destring = f.quoted for _, i := range f.index { if subv.Kind() == reflect.Ptr { if subv.IsNil() { subv.Set(reflect.New(subv.Type().Elem())) } subv = subv.Elem() } subv = subv.Field(i) } 以上只是一个简单的演示，主要是关于，JSON 包中反射是如何使用的。如果希望自己阅读代码，源码在 encoding/json/decode.go。\n记忆与短期记忆 # Json Unmarshal 是反射的其中一个使用案例。还有其他场景吗？接下来，我们将用反射开发一个库，基于 \u0026ldquo;记忆法\u0026rdquo; 实现短期缓存。\n或许你并不熟悉这个术语，\u0026ldquo;记忆\u0026rdquo; 出自于函数式编程。函数式编程中倾向于强制推行某些规则，比如，参数和变量通常是不可修改的，创建之后便不可变。函数式编程尝试限制编程的 \u0026ldquo;副作用\u0026rdquo;。\n一个实际的程序基本是不可能的没有 \u0026ldquo;副作用\u0026rdquo; 的，因为总会涉及诸如信息打印、写文件、向数据库插入数据等事情。但其他的一些 \u0026ldquo;副作用\u0026rdquo;，比如更新全局变量，将会导致程序很难追踪。函数式编程的一个目标，让程序中的数据追踪变得简单，我们可以非常容易地就明白程序在做什么？\n函数式编程还有一些其他好处。比如，当一个函数输入和返回不变且没有 \u0026ldquo;副作用\u0026rdquo; 时，每次调用函数，相同的输入，做同样的工作，返回相同的结果。如果我们保存了这些结果，重复的工作就没有必要做第二次了。\n到此，我们开始引入 \u0026ldquo;记忆\u0026rdquo; 的概念。它类似于函数级别的缓存。\u0026ldquo;记忆\u0026rdquo; 是通过在恒定的函数上包裹函数，实现输入输出的缓存，从而避免重复不必要的工作。但一个函数被 \u0026ldquo;记忆\u0026rdquo;，对于一个输入，只有执行一次工作。相同的输入执行第二次函数调用时，返回值将从缓存中获取，而不是重新计算一次。对于那些复杂或非常慢的操作，如此对性能的提升将是非常大的。\nGo 可能不算是函数式编程语言，但我们依然可以通过它践行自己的想法。这种编程风格稍微有点严格，但是它避免了输入输出的更新，最小化了程序的 \u0026ldquo;副作用\u0026rdquo;，使你的程序非常易读。\n我们将实现一小短时间的缓存，而不是永远。在微服务架构中，这是一种更加通用的模式。比如如下的场景：\n一个服务提供数据，另一个服务获取数据。因为数据是通过网络传递的，会占用一定时间。这将会降低系统的整体性能。如果数据不是经常改变的且数据延迟几秒更新也没有关系，那么可以暂时缓存这个数据，它将给你的系统带来一个显著的性能提升。通过函数式编程的 \u0026ldquo;记忆\u0026rdquo;，我们可以在也没有对太多 API 的修改实现缓存，避免系统额外的网络调用。\n如何实现？我们将通过反射实现三件事：\n确保输入类型是一个函数，并且至少包含一个输入和一个输出； 确保新创建结构体中的字段类型和输入参数的类型一一对应； 确保新创建函数的输入和输出参数和原始函数相匹配； 还有一个限制，所有的输入参数必须可比较，在 Go 中，可比较的类型及可以通过 == 符号进行布尔运算。我们将用 Go 中的映射 map 关联输入和输出，Go 中 map 的 key 必须是可比较的，这很有意义，因为要确认输入参数是否出现过，我们需要检查前后的相等性。\n幸运的是，Go 中只有四种情况是不可比较的，如下：\n切片，Slices； 映射，Maps； 函数，Functions； 结构体，成员包含 Slice、Map 或 Function； 让我们开始定义 Cacher 函数，类似如下的形式：\n// Takes in a function and a time.Duration and returns a caching version of the function // 接收两个参数，分别是函数和时间，返回的是一个缓存版本的函数 // // The limitations on memoization are: // 限制如下： // // — there must be at least one in parameter // - 必须有一个输入参数 // // — there must be at least one out parameter // - 必须有一个输出参数 // // — the in parameters must be comparable. That means they cannot be of kind slice, map, or func, // nor can input parameters be structs that contain (at any level) slices, maps, or funcs. // - 输入参数必须是可比较的， // // Be aware that if your memoized function has any side-effects (does anything that isn’t // reflected in the output, like print to the screen or write to a database) the side-effects // will be performed by the function only the first time that the function is invoked with // particular set of values. // 要明白了解，如果被记忆的函数有任何的副作用（指任何不能在返回结果中反应出来的东西，比如打印、写数据库），这些 \u0026#34;副作用\u0026#34; // 将只会执行一次。 func Cacher(f interface{}, expiration time.Duration) (interface{}, error) { return f, nil } 代码并没有做什么，但通过它，我们明白了接下来的目标。下面，我们正式开始填写代码，首先通过反射检查，我们传递的确实是一个函数。\nfunc Cacher(f interface{}, expiration time.Duration) (interface{}, error) { ft := reflect.TypeOf(f) if ft.Kind() != reflect.Func { return nil, errors.New(\u0026#34;Only for functions\u0026#34;) } return f, nil } 接下来，创建一个结构体用来存放我们的输入参数。在创建结构体时，我们需要保证必须有一个输入和一个输出，并且所有的输入都是可比较的。\nunc buildInStruct(ft reflect.Type) (reflect.Type, error) { if ft.NumIn() == 0 { return nil, errors.New(\u0026#34;Must have at least one param\u0026#34;) } var sf []reflect.StructField for i := 0; i \u0026lt; ft.NumIn(); i++ { ct := ft.In(i) if !ct.Comparable() { return nil, fmt.Errorf(\u0026#34;parameter %d of type %s and kind %v is not comparable\u0026#34;, i+1, ct.Name(), ct.Kind()) } sf = append(sf, reflect.StructField{ Name: fmt.Sprintf(\u0026#34;F%d\u0026#34;, i), Type: ct, }) } s := reflect.StructOf(sf) return s, nil } func Cacher(f interface{}, expiration time.Duration) (interface{}, error) { ft := reflect.TypeOf(f) if ft.Kind() != reflect.Func { return nil, errors.New(\u0026#34;Only for functions\u0026#34;) } inType, err := buildInStruct(ft) if err != nil { return nil, err } if ft.NumOut() == 0 { return nil, errors.New(\u0026#34;Must have at least one returned value\u0026#34;) } fmt.Println(\u0026#34;inType looks like\u0026#34;, inType) return f, nil } 接下来，还剩最后一步，定义一个 map 变量，用它存放输入输出的缓存，并且用 reflection 反射在 f 函数基础上生成具有缓存能力的新函数。\ntype outExp struct { out []reflect.Value expiry time.Time } func Cacher(f interface{}, expiration time.Duration) (interface{}, error) { ft := reflect.TypeOf(f) if ft.Kind() != reflect.Func { return nil, errors.New(\u0026#34;Only for functions\u0026#34;) } inType, err := buildInStruct(ft) if err != nil { return nil, err } if ft.NumOut() == 0 { return nil, errors.New(\u0026#34;Must have at least one returned value\u0026#34;) } m := map[interface{}]outExp{} fv := reflect.ValueOf(f) cacher := reflect.MakeFunc(ft, func(args []reflect.Value) []reflect.Value { iv := reflect.New(inType).Elem() for k, v := range args { iv.Field(k).Set(v) } ivv := iv.Interface() ov, ok := m[ivv] now := time.Now() if !ok || ov.expiry.Before(now) { ov.out = fv.Call(args) ov.expiry = now.Add(expiration) m[ivv] = ov } return ov.out }) return cacher.Interface(), nil } 完成！\n再来看一下上面的代码，首先，我们定义了一个结构体 outExp，用它存放输出和过期时间。\n接着，我们定义了一个 map，它的 key 是interface{}，值是 outExp 类型。它们的选择都是有原因的。先说 key 是 interface{} 类型的原因。之前的例子，我们通过反射创建了一个结构体，这种结构体没有名称，为了存储实例，我们不得不使用 interface{} 类型表示。关于返回类型，当你用反射调用函数，它的返回类型是 []reflect.Value。同样，传递给 MakeFunc 的闭包函数返回的也是这种类型的值。 为了避免值拷贝，我们通过 []reflect.Value 保存返回值，并把它存入 map。\n在闭包中，我们通过反射构造了一个自定义类型的实例，将传给函数的参数放入其中。接着，检查 m 中是否存在实例等于它，如果没有，或已经过期，我们将调用包裹函数，然后将响应结果和过期时间保存进变量 ov 中。接着，以自定义结构体的实例为 key，将 ov 保存进 m 中。最后，返回 ov.out 即可。\n到此，我们正式完成了 Cacher 工厂函数，它可以包裹 Go 中几乎所有的函数，实现一定期限的缓存。\n我们如何使用呢？一个例子，如下：\nfunc AddSlowly(a, b int) int { time.Sleep(100 * time.Millisecond) return a + b } func main() { ch, err := Cacher(AddSlowly, 2*time.Second) if err != nil { panic(err) } chAddSlowly := ch.(func(int, int) int) for i := 0; i \u0026lt; 5; i++ { start := time.Now() result := chAddSlowly(1, 2) end := time.Now() fmt.Println(\u0026#34;got result\u0026#34;, result, \u0026#34;in\u0026#34;, end.Sub(start)) } time.Sleep(3 * time.Second) start := time.Now() result := chAddSlowly(1, 2) end := time.Now() fmt.Println(\u0026#34;got result\u0026#34;, result, \u0026#34;in\u0026#34;, end.Sub(start)) } 例子中，仅仅是 sleep 100ms，然后，求两个数的和。实际的情况可能是，数据库的查询或网络服务的调用。由于 Go 没有泛型，Cacher 返回的函数需要转化为合适的类型，错误检查也要求有几行代码，interface{} 的 ch 也需要转化为实际的函数类型。\n执行代码，将会得到如下的输出：\n$ go run cacher.go got result 3 in 100.079405ms got result 3 in 3.873µs got result 3 in 561ns got result 3 in 462ns got result 3 in 398ns got result 3 in 100.054602ms 第一次执行占用了大概 100 ms（计算处理时间），接下里的几次调用都只用了几百纳秒。在暂停了 3 秒后，执行最后一次，再次耗时 100 ms。\n运行示例\n新的秘密武器 # 再说最后一点，反射对性能有一定的影响。如果执行的是密集性运算，或是调用网络服务，通过反射在上面加一层代码，这对性能将不会有太大的影响。但是，多数代码都是非常快的。极有可能，你代码中的大部分方法的执行时间都是几百毫秒以内，此类场景，就要小心反射的使用了，此时，反射会性能有较大影响，我们需要重新思考是否值得。\n总而言之，当我们再遇到各种类型的问题时，可以回想下反射提供的可能。当遇到看起不可能解决的问题时，比如虽然两个类型的处理逻辑相似，但是类型的自身的共性不多，这时候，反射将成为你的秘密武器。\n我的博文：Go 中如何使用反射 Part Two，译：Learning to Use Go Reflection — Part 2\n","date":"2019-08-17","externalUrl":null,"permalink":"/posts/2019-08-17-reflection-in-golang-part2/","section":"文章","summary":"译者前言 # 这篇博文介绍的内容比较实在，主要是关于两方面的内容。一是介绍 reflection 在 encoding/json 中的应用，另一个开发了一个 Cacher 工厂函数，实现函数式编程中的记忆功能，其实就是根据输入对输出进行一定限期的缓存。\n","title":"Go 中如何使用反射 Part Two","type":"posts"},{"content":"Go 的源码在安装包的 src/ 目录下。怎么看它的源码呢？直接看吧！没人教的情况下，只能自己撸了。当然，这种内容一般也不会有人教。\n怎么撸？\nGo 源码中，应该可分为与语言息息相关的部分，和官方提供的标准库。与语言实现相关的肯定是最难的，不是那么容易理解。可以先主要看标准库，其他的可以先大概了解下。\n先把源码目录整体扫一遍，大概看看涉及了哪些模块，然后再挑自己喜欢的部分进行更深一步的学习与研究。建议每个目录都简单写个 hello world，如此的体悟会更深。如果连 hello world 也写不出来，这个模块的源码暂时就没必要研究了，先学好基础吧。毕竟，包的使用不仅与语言相关，还涉及具体场景和实现原理，这都是要学习的。\n对包的使用熟悉理解后，就可以阅读源码了，但此时最好还是不要太抠细节，求理解涉及设计思想，整体流程。源码阅读可以通过画 UML 的方式辅助，从纵向和横向帮助理解。代码设计时，一般最容易想到的就是按顺序方式写，很快就能搞定。但当项目变大，抽象的模块会越来越多，抽象出接口和具体的实现，实现可能包含其他类型的组合。搞明白这些关系，对于理解源码实现会较有帮助。\n如果能顺利经过前面两步，接下来的源码阅读就比较简单了。而且 Go 语言的特点就是简洁易读，没什么语法糖。当然，如果是一些实现比较复杂的包，你还需知道它们的底层原理，就比如 net/http 包，你得对 http 协议熟悉到一定程度，才可能从细节研究源码实现。\n可能是我闲的蛋疼，准备试着先从第一步出发，整体撸一下 Go 的源码中包含的模块，没事的时候就更新一点进去。等把这些大致撸完一遍，感觉我的 Golang 之旅 专栏又可以多出很多写作素材了。\n我的环境是 Go 1.11。关于每个模块，我会把读过的一些文章放在下面，由于只是粗略阅读，并不能保证读过的每篇文章都是精品。\n补充：\n2019年8月8日 凌晨 01:13， 大概花了两个多星期的零碎时间，简单撸完了一版。总的感觉，还是有很多地方理解不够，希望后面可以按前面说的思路，按包逐步进行源码解剖。\narchive # 包含了文件归档的相关内容，其中涉及了两个包，分别是 tar 和 zip。\narchive/tar，即归档，如果了解 Linux 下的 tar 命令，可与之对应理解。如果要在归档基础上进行压缩，还要借助 compress 下的相关包。提醒一点，是使用时要注意理解归档与压缩的区别。\n相关阅读：\n鸟哥的文件与文件系统的压缩与打包\narchive/tar 实现打包压缩及解压\narchive/zip，与 zip 格式压缩文件操作相关的包，使用方法与 tar 很类似。在寻找与 zip 包相关的资料时，了解到 zip 的作者年仅 37 岁就逝世了，而全世界所有使用 zip 压缩的文件开头部分都有他的名字 \u0026ldquo;PK\u0026rdquo;，而我们识别一个文件是否是 zip 正是通过这种方法。\n相关阅读：\narchive/zip 实现压缩与解压\nzip 的百度百科\nbufio # 实现了缓冲 IO 的功能，通过包裹 io.Reader 或 io.Writer 函数创建新的 Reader 或 Writer 实例，并且这些新创建的实例提供了缓冲的能力。使用方法非常简单，达到指定缓冲大小，触发写或读操作，如未达到要求，可用 Flush 方法刷新。\n相关阅读：\nIntroduction to bufio package in Golang\nIn-depth introduction to bufio.Scanner in Golang\nbufio - 缓存 IO\nbuiltin # Go 语言中的内置类型、函数、变量、常量的声明。暂时看来，没啥可深入阅读的，应该结合 Go 的内部实现进行阅读。\nbytes # 主要是关于 byte slice 操作的一些函数。由于 []byte 也可用于表示 string，故其中的函数、方法与 strings 很类似，比如 Join、Split、Trim、 Contains 等。\n相关阅读：\nGo 语言学习 - bytes 包\nGo Walkthrough: bytes+ + strings\ncmd # Go 命令工具集的实现代码，如 go、gofmt、godoc、pprof 等，应该主要是和 Go 语言实现相关性较大，比较底层。每个命令都够研究一段时间了，特别是 go 命令，并且前提是你的计算机底层原理的功底要足够优秀。\n网上搜索下，关于它的资料比较少。\n相关阅读：\nGo 官网之 Command go\ncompress # 之前提到 archive 包中是归档相关操作，而相对的 compress 包主要与压缩相关。主要实现了几种主流的压缩格式，如 bzip2、flate、gzip、lzw、zlib。\ncompress/bzip2，常见的 .bz2 结尾的压缩文件格式基本可用这个包操作，要与 tar 结合使用。\ncompress/gzip，常见的 .gz 结尾的压缩文件格式基本可用这个包操作，要与 tar 结合使用。\ncompress/flate，flate 应该主要是 zip 用的压缩算法，如果阅读了前面的 archive/zip 的源码，就会发现其中导了这个包。\ncompress/zlib， compress/lzw 基本与上面同理，应该都是某种压缩算法实现。因为我对压缩算法没什么太深的研究，暂时了解个大概就好了，希望没有介绍错误。\n相关阅读：\nGo 官网之 compress\ncontainer # 我们知道，Go 内置的数据结构很少，只有数组、切片和映射。除此以外，其实还有部分的结构放在了 container 包中，heap 堆、list 双端队列，ring 回环队列。\n它们的使用非常简单，基本就是增删改查。\n相关阅读：\ncontainer 容器数据类型：heap、list 和 ring\ncontext # 读这个包之前，得首先熟悉 Go 的并发代码如何编写，了解 Done channel 如何实现向所有 goroutine 发送广播信号。Go 的并发单元称为 goroutine，但是不同 goroutine 之间并没有父子兄弟关系，为了更好地并发控制，context 包就诞生了。它可以实现在不同 goroutine 间安全地传递数据以及超时管理等。\n相关阅读：\nGo 译文之通过 context 实现并发控制\n深度解密 Go 语言之 Context\ncrypto # 加密相关，涉及内容有点多，包含了各种常用的加密算法实现，比如对称加密啊 AES、DES 等，公私钥加密 rsa、dsa 等，散列算法 sha1、sha256 等，随机数 rand 也有，不知道和 math 的随机有什么区别。没有找到一篇综合性介绍的文章，毕竟比较复杂了，如果要看它们的源码，得先要大概了解下每个加密算法的原理，才好逐一突破。\n相关阅读：\nGo 官网之 crypto\ndatabase # 封装了一套用于数据库操作的通用接口，实现了数据库连接管理，支持连接池功能。真正使用时，我们需要引入相应的驱动，才能实现指定类型数据库的操作。\n一个简单的例子。\nimport ( \u0026#34;database/sql\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; ) func main() { db, err := sql.Open(\u0026#34;mysql\u0026#34;, \u0026#34;username:password@tcp(127.0.0.1:3306)/test\u0026#34;) if err != nil { log.Fatal(err) } defer db.Close() } github.com/go-sql-driver/mysql 便是提供的 MySQL 驱动。具体的查询执行都是通过调用驱动实现的 db 接口中的方法。\n相关阅读：\ndatabase/sql-SQL/SQL-Like 数据库操作接口\n关于Golang中database/sql包的学习笔记\ndebug # 和调试相关，具体内容比较复杂，我也不是很懂。内部有几个包，如 dwarf、elf、gosym、macho、pe、plan9obj。\ndwarf，可用于访问可执行文件中的 DWARF 信息。具体什么是 DWARF 信息呢？官网有个 PDF，具体介绍了什么是 DWARF，有兴趣可以看看。它主要是为 UNIX 下的调试器提供必要的调试信息，例如 PC 地址对应的文件名行号等信息，以方便源码级调试。\n相关阅读：\ndwarf-2.0.0 调试信息格式 DWARF, 说不定你也需要它哦\nelf，用于访问 elf 类型文件。elf，即可执行与可连接格式，常被称为 ELF 格式，有三种类型：\n可重定位的对象文件（Relocatable file），由汇编器汇编生成的 .o 文件 可执行性的对象文件（Executable file），可执行应用程序 可被共享的对象文件（Shared object file），动态库文件，也即 .so 文件 相关阅读：\nreadelf elf文件格式分析 The 101 of ELF files on Linux: Understanding and Analysis\ngosym，用于访问 Go 编译器生成的二进制文件之中的 Go 符号和行信息等，暂时还没怎么看。在 medium 发现个系列文章，介绍了 Go 中 debug 调试器的实现原理，相关阅读部分是系列的第二篇文章。\n相关阅读：\nMaking debugger in Golang (part II)\nmacho，用于访问 Mach-O object 格式文件。要阅读这段源码，同样需要先了解什么是 Mach-O，它是 Mach object 文件格式的缩写，用于可执行文件、目标代码、内核转储的文件格式。\n相关阅读：\n维基百科-Mach-O\nGo package - debug/macho\npe，实现了访问 PE 格式文件，PE 是 Windows 系统可移植的可执行文件格式。\n相关阅读：\nWIKI - Portable Executable\nGo package - debug/pe\nplan9obj，用于访问 plan9 object 格式文件。\n暂未找到关于 plan9object 的介绍文章。我们主要学习的话，主要应该是集中在 elf 和 gosym 两个格式。\n相关阅读：\nGo package - debug/plan9obj\nencoding # 主要关于我们常用到的各种数据格式的转化操作，或也可称为编解码，比如 JSON、XML、CSV、BASE64 等，主要的模块有：\nencoding/json，json 处理相关的模板，通用方式，我们可以将解析结果放到 map[string]interface{} 解析，也可以创建通用结构体，按 struct 方式进行。\nencoding/xml，基本和 encoding/json 类似。但因为 XML 比 json 要复杂很多，还涉及一些高级用法，比如与元素属性相关等操作。\nencoding/csv，csv 数据格式解析。\nencoding/binary，可用于处理最底层的二进制数据流，按大小端实现 []byte 和整型数据之间的转化。\n其他诸如 hex、gob、base64、base32、gob、pem、ascii84 等数据格式的操作都是类似的，有兴趣可以都尝试一下。\n相关阅读：\nGolang 下的 encoding 相关模块的使用 Go 标准库文档翻译之 encoding/xml Golang 中 byte 转 int 涉及到大小端问题吗 使用 Go 语言标准库对 CSV 文件进行读写 Go Walkthrough: encoding package\nerrors # Go 的错误处理主要代码就是它。很遗憾的是，打开源码后发现，就几行代码哦。主要是因为 Go 的错误类型只是一个接口而已，它的源码非常简单。\npackage errors // New returns an error that formats as the given text. func New(text string) error { return \u0026amp;errorString{text} } // errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return e.s } Go 默认只提供了最简单的实现，就上面这几行代码。真的是 awesome、amazing，哈哈。但正是因为简单，扩展出自己的 error 变得很简单。比如，有些开发者认为 Go 的错误处理太简单，开发了一些包含 call stack trace 的 error 包。\n相关阅读：\ngithub.com/pkg/errors\nerror-handling-and-go\nWhat I Don’t Like About Error Handling in Go\nexpvar # 主要是用于 Go 程序运行时的指标记录，如 HTTP 服务在加入 expvar 后，我们可以通过 /debug/vars 返回这些指标，返回的数据是 JSON 格式的。\n它的源码不多，也就大约 300 行代码，重点在它使用方法。\n相关阅读：\n标准库 EXPVAR 实战 Monitoring apps with expvars and Go\nflag # 用于命令行参数解析的包，比如类似命令参数 grep -v grep，具体操作的时候要获取 -v 后的参数值。很常用的功能，如果纯粹自己实现是比较繁琐的。\n相关阅读：\nflag-命令行参数解析\nfmt # 从包名就可以知道，fmt 主要和格式化相关，关于什么的格式化呢？主要是字符串的格式化，它的用法和 C 中 printf 都很类似。当然，除了实现 C 的用法，还提供了一些 Go 特有的实现。\n相关阅读：\nGo Walkthrough: fmt\ngo # 似乎是核心工具使用的包。\nhash # hash 包主要定义了不同的 hash 算法的统一接口。而具体的 hash 算法实现有的直接 hash 的下层，比如 crc32、crc64，即 32 位循环冗余校验算法和 64 位循环冗余校验算法。而 md5 hash 算法在 crypto/md5 下，同样实现了 hash 的相关接口。\n相关阅读：\n常见哈希函数 FNV 和 MD5\nhtml # Go 标准库里的 html 包功能非常简单，大概了看下，主要是关于 html 文本的处理，例如该如何对 html 代码做转义。如果想支持 html 的解析，go 官方 github 下还提供了一个 net 仓库，其中有个 html 的工具包。而 goquery 也是基于它实现的。\n标准库的 html 目录下还有 template，html 的模板渲染工具，通过与 net/http 相结合，再加上一个数据库 orm 包，简单的 web 开发就可以开始了。\n相关阅读：\nEasy way to render HTML in Go\nGo 语言解析 html\nimage # Go 2D 图像处理库，支持创建 2D 处理的方法函数，图片创建、像素、颜色设置，然后进行绘制。主要支持 png、jpeg、gif 图片格式。\n相关阅读：\ngolang中image包用法\ngolang 中 image/draw 包用法\nGolang 绘图技术\nindex # 目录为 index，其中只有一个包 index/suffixarray，称为后缀数组。具体算法没仔细研究，大致是将子字符串查询的时间复杂度降低到了 $log_n$。\n使用非常简单，官网已经提供了一个例子。\n// create index for some data index := suffixarray.New(data) // lookup byte slice s offsets1 := index.Lookup(s, -1) // the list of all indices where s occurs in data offsets2 := index.Lookup(s, 3) // the list of at most 3 indices where s occurs in data 相关阅读：\nGo package - index/suffixarray suffix array 后缀数组算法心得\ninternal # 内部实现，比较复杂。\nio # Go 的标准库中，为 io 原语提供了基本的接口和实现，帮助字节流的读取。接口主要就是 io.Reader 和 io.Writer。io 包提供了一些常用资源的接口实现，比如内存、文件和网络连接等资源进行操作。\n阅读 io 包的源码，会发现很多接口都是基于具体的能力定义，最简单的有 Reader（读）、Writer（写）、Closer（关闭）、Seeker（偏移），一个接口一个方法，非常灵活。组合的接口还有 ReaderWriter（读写）、ReadeCloser（读与关）、WriteCloser（读写关） 和 ReadWriteCloser（读写关）等。整体理解，我们将会对 Go 接口是基于是鸭子模型的说法更有体会，\n相关阅读：\nGo 中 io 包的使用方法\n基本的 IO 接口\nStreaming IO in Go\nlog # Go 的日志包，通过记录日志可以方便我们进行问题调试。log 包的核心源码并不多，总共也就三百多行，其中注释就占了差不多一百行。主要是因为它提供的功能很少，只有基础的日志格式化，还有 Print、Panic、Fatal 三种日志打印函数。连错误级别没提供。如果要使用的话，还需要借助一些第三方的包。相关阅读中提供了一个 \u0026ldquo;Go 日志库集合\u0026rdquo; 的文章，具体我也没有深入研究。\n相关阅读：\nGo log 日志\nGo 日志库集合\nmath # 主要是关于数学计算方面的函数，一些数学常量，比如 PI（圆周率）、E（自然对数）等，就在其中，还有如四舍五入方面的函数 Round、Floor、Ceil、最大值 Max、最小值 Min，复杂的数学运算，比如幂运算、对数、三角函数肯定也有的，其他诸如随机数之类的函数也在其中。打开 math 源码文件夹，发现里面有大量的汇编代码，数学相对片底层，对性能要求会比较高，有必要用汇编实现。\nmath 包，直接看官方文档就好了，一般看了就可以用，没什么业务场景、具体原理需要了解，毕竟大家都学过数学。如果要看汇编实现，那就复杂了。有兴趣可以研究一下。\n相关阅读：\nGo 官网 math\nmime # 要了解 mime 包的使用，得先了解什么是 MIME，全称 Multipurpose Internet Mail Extension，即多用途互联网邮箱扩展类型。最初设计的目标是为了在发送邮件时，附加多媒体内容。后来，MIME 在 HTML 中也得到了支持。\n其中主要有四个函数，AddExtensionType、TypeByExtension、FormatMediaType 和 ParseMediaType。前后两组函数似乎都是针对 MediaType 的互操作。\n相关阅读：\nGo 标准库学习 mime\nGo package - mime\nnet # 网络相关，涉及内容比较多，有种吃不消的感觉。\n底层的实现 socket 就在 net 包下，主要是一些底层协议的实现，比如无连接的 ip、udp、unix(DGRAM)，和有连接的 tcp、unix(STREAM) 都可以在 net 包找到。\n应用层协议，http 协议实现在 net/http 包含客户端服务端，rpc 在 net/rpc，邮件相关的 net/mail、net/smtp 等。net/url 是与 url 处理相关的函数，比如 url 字符串解析，编码等。\n相关阅读：\ngolang net 包学习笔记 Go 官方库 RPC 开发指南 Go 爬虫必备之 HTTP 请求 QuickStart Sending HTML emails using templates in Golang\nos # os 包主要实现与操作系统相关的函数，并且是与平台无关的。它的设计是 UNIX 风格的，并且采用 Go 错误处理风格。发生错误将返回的 error 类型变量。比如 Open、Stat 等操作相关的函数。\nos 包的目标是统一不同操作系统的函数。如果大家读过那本 UNIX 环境高级编程，你会发现 os 包中的函数与 Unix 的系统调用函数都很相似。\n除了 os 包，该目录下还有几个包，分别是 os/exec、os/signal 和 os/user，如下：\nos/exec，帮助我们实现了方便执行外部命令的能力。\nos/signal，Unix-Like 的系统信号处理相关函数，Linux 支持 64 中系统信号。\nos/user，与系统用户相关的库，可用于获取登录用户、所在组等信息。\n相关阅读：\n[译]使用 os/exec 执行命令 Go package - os\npath # path 包实现了路径处理（通过 / 分隔）相关的一些常用函数，常用于如文件路径、url 的 path。不适合 Windows 的 \\ 和磁盘路径处理。\n主要包含的函数有 Base、Clean、Dir、Ext、IsAbs、Join 等函数。如 Base 可用于获取路径的最后一个元素，Dir 获取路径目录，Ext 获取文件扩展、IsAbs 判断是否为绝对路径，Join 进行路径连接等。\n相关阅读：\nGo package - path\nplugin # plugin 包是 Go 1.8 出现的包，为 Go 增加了动态库加载的能力，当前只支持 Linux 和 MacOS。但这个包的应用并不是很方便，生成和使用库文件的环境有一定的要求。\n相关阅读：\n如何评价 Go 标准库中新增的 plugin 包\nwriting-modular-go-programs-with-plugins\ncalling-go-functions-from-other-languages\ngosh-a-pluggable-command-shell-in-go\nreflect # 与反射相关的函数函数，通过反射可以实现运行时动态创建、修改变量，进行函数方法的调用等操作，获得本属于解释语言的动态特性。要阅读反射包源码，重点在理解变量的两个组成，即类型和值，反射的核心操作基本都是围绕它们进行。reflect.ValueOf 与 reflect.TypeOf 是我们常用的两个方法。\n相关阅读：\nGo 译文之如何使用反射 Go 译文之如何使用反射（二） Go package - reflect\nregexp # Go 的正则包，用于正则处理。基本是每种语言都会提供。其中涉及的方法大致可分为几个大类，分别是 Compile 编译、Match 匹配、Find 搜索、Replace 替换。\n正则的源码实现还真是不想看。感觉正则都没有完全理清楚，扯源码有点坑。特别头大。\n相关阅读：\nGo 官网之 regexp Golang-Regex-Tutorial\nruntime # runtime 是与 Go 运行时相关的实现，我们可以通过它提供的一些函数控制 goroutine。关于 Go 进程的启动流程、GC、goroutine 调度器等，也是在 runtime 中实现，同样需要我们好好阅读 runtime 代码了解。除此以为，cgo、builtin 包的实现也是在 runtime。\n相关阅读：\n说说 Golang 的runtime golang internals Go package - runtime\nsort # 定义了排序的接口，一旦某个类型实现了排序的接口，就可以利用 sort 中的函数实现排序。通过阅读源码，我发现默认支持排序的类型包括 int、float64、string。sort 中还有个 search 文件，其中主要是已排序内容二分查找的实现。\n我们都知道，排序算法很多，比如插入排序、堆排序与快速排序等，sort 包都已经实现了，并且不用我们决定使用哪种算法，而是会依据具体的数据决定使用什么算法，并且一次排序不一定只要了一种算法，而可能是多种算法的组合。如何做算法选择可以通过阅读 sort.go 文件中的 quickSort 函数了解。\n相关阅读：\nSort - 排序算法 The 3 ways to sort in Go\nstrconv # 关于字符串与其他类型转化的包，名字全称应该是 string convert，即字符串转化。比如整型与字符串转化的 Itoa 与 Atoi，浮点型与字符串的转化 ParseFloat 与 FormatFloat，布尔型与字符串转化 ParseBool 与 FormatBool 等等。\n相关阅读：\nGolang 中 strconv 的用法 官方文档 - strconv\nstrings # 针对字符串的操作函数，前面也提过到，因为 []byte 也可用于表示字符串，strings 中的很多函数在 bytes 包也有类似的实现，比如 Join、Split、Trim，大小写转化之类的函数等。\n相关阅读：\nstrings - 字符串操作\nsync # Go 推荐以通信方式（channel）实现并发同步控制，但传统机制也是支持的，比如锁机制、条件变量、WaitGroup、原子操作等，而它们都是由 sync 提供的。其中，原子操作在 sync/atomic 包下。\n除此之外，sync 中还有个临时对象池，可以实现对象复用，并且它是可伸缩且并发安全的。\n相关阅读：\n浅谈 Golang sync 包的相关使用方法\nsyscall # 系统调用，从名字就能知道，这个包很复杂。系统调用是实现应用层和操作底层的接口，不同系统之间的操作常常会有一定的差异，特别是类 Unix 与 Windows 系统之间的差异较大。\n如果想要寻找 syscall 的使用案例，我们可以看看 net、os、time 这些包的源码。\n如果要看这部分源码，当前的想法是，我们可以只看 Linux 的实现，架构的话，如果想看汇编，可以只看 x86 架构。\n暂时研究不多，不敢妄言。\n相关阅读：\n视频笔记：Go 和 syscall\nGo package - syscall\ntesting # Go 中测试相关的实现，比如单元测试、基准测试等。Go 推荐的测试方式采用表格驱动的测试方式，即非每种情况都要写一个单独的用例，而是通过列举输入、期望输出，然后执行功能并比较期望输出与实际输出是否相同。\n一个简单的测试用例。\nfunc TestSum(t *testing.T) { var sumTests = []struct { a int b int expected int }{ {1, 1, 2}, {2, 1, 3}, {3, 2, 5}, {4, 3, 7}, {5, 5, 10}, {6, 8, 14}, {7, 13, 20}, } for _, tt := range sumTests { actual := functions.Add(tt.a, tt.b) if actual != tt.expected { t.Errorf(\u0026#34;Add(%d, %d) = %d; expected %d\u0026#34;, tt.a, tt.b, actual, tt.expected) } } } 相关阅读：\n单元测试\n基准测试\nGo package - testing\ntext # 主要是关于文本分析解析的一些包，但又不同于字符串处理，主要涉及词法分析 scanner、模板引擎 template、tab 处理 tabwriter。\ntext/scanner，主要是做词法分析的，如果大家读过我的专栏翻译的几篇关于词法分析的文章，对它的理解会比较轻松。\ntext/template，用于文本的模板处理，相对于 html/template 的具体应用场景，text/template 更通用。要熟悉使用它，还需要掌握它的一些方法，比如 Action、Argument、Pipeline、Variable、Function。\ntext/tabwriter，感觉没啥介绍的，好像主要是根据 tab 进行文本对齐的。\n相关阅读：\ntext/template A look at Go lexer/scanner packages\nGo 模板嵌套最佳实践\nPackage tabwriter\ntime # 关于日期时间的包，Go 中的 unix timestamp 是 int64，表示的时间范围相应的也就有所扩大。其他的诸如睡眠、时区、定时控制等等都支持，Go 中有个逆人性的规则，那就是日期时间的格式化字符，比如传统语言的格式化字符串 YYYY-MM-DD 在 Go 却是 2006-01-02 的形式，奇葩不奇葩。\n相关阅读：\nGo 标准库\u0026ndash;time 常用类型和方法\nGo 时间、时区、格式的使用\ngolang package time \b用法详解\nunicode # unicode 编码相关的一些基本函数，读源码会发现，它通过把不同分类字符分别到不同的 RangeTable 中，实现提供函数判断字符类型，比如是否是控制字符、是否是字母等。另外两个包 unicode/utf8 和 unicode/utf16 可用于 unicode (rune) 与 utf8 (byte)、unicode (rune) 与 utf16 (int16) 之间的转化。\n相关阅读：\ngo package 之 unicode Unicode 码点、UTF-8/16编码\nunsafe # Go 语言限制了一些可能导致程序运行出错的用法，通过编译器就可以检查出这些问题。当然，也有部分问题是无法在编译时发现的，Go 给了比较优化的提示。但通过 unsafe 中提供的一些方法，我们可以完全突破这一层限制，从包名就可以知道，unsafe 中包含了一些不安全的操作，更加偏向于底层。一些比较低级的包会调用它，比如 runtime、os、syscall 等，它们都是和操作系统密切相关的。我们最好少用 unsafe，因为使用了它就不一定能保证程序的可移植性或未来的兼容性问题。\n相关阅读：\nGo 圣经 - 底层编程\nGo package - unsafe\nvendor # 标准库中依赖的第三方包，当然也都由 Go 官方所开发，默认包括的依赖有：\ngolang_org/x/crypto golang_org/x/net golang_org/x/text 举个例子，加密相关的 crypto 包中实现就用到了 golang_org/x/crypto/curve25519 中的方法。\n除了内置标准库，官方还提供了其他很多库，诸如 crypto、net、text 之类的包。具体可查看 Go 官方 github 地址。\n博文地址：如何阅读 Go 源码\n","date":"2019-08-15","externalUrl":null,"permalink":"/posts/2019-08-15-how-to-read-golang-source-code/","section":"文章","summary":"Go 的源码在安装包的 src/ 目录下。怎么看它的源码呢？直接看吧！没人教的情况下，只能自己撸了。当然，这种内容一般也不会有人教。\n怎么撸？\nGo 源码中，应该可分为与语言息息相关的部分，和官方提供的标准库。与语言实现相关的肯定是最难的，不是那么容易理解。可以先主要看标准库，其他的可以先大概了解下。\n","title":"如何阅读 Go 源码","type":"posts"},{"content":" 什么是反射 # 多数情况下，Go 中的变量、类型和函数的使用都是非常简单的。\n当你需要一个类型：\ntype Foo struct { A int B string } 当你需要一个变量，定义如下：\nvar x Foo 当你需要一个函数，定义如下：\nfunc DoSomething(f Foo) { fmt.Println(f.A, f.B) } 但有时候，你想使用的变量依赖于运行时信息，它们在编程时并不存在。比如数据来源于文件，或来源于网络，你想把它映射到一个变量，而它们可能是不同的类型。在这类场景下，你就需要用到反射。反射让你可以在运行时检查类型，创建、更新、检查变量以及组织结构。\nGo 中的反射主要围绕着三个概念：类型（Types）、类别（Kinds）和值（Values）。反射的实现源码位于 Go 标准库 reflection 包中。\n检查类型 # 首先，让我们来看看类型（Types）。你可以通过 reflect.TypeOf(var) 形式的函数调用获取变量的类型，它会返回一个类型为 reflect.Type 的变量，reflect.Type 中的操作方法涉及了定义该类型变量的各类信息。\n我们要看的第一个方法是 Name()，它返回的是类型的名称。有些类型，比如 slice 或 指针，没有类型名称，那么将会返回空字符串。\n下一个介绍方法是 Kind()，我的观点，这是第一个真正有用的方法。Kind，即类别，比如切片 slice、映射 map、指针 pointer、结构体 struct、接口 interface、字符串 string、数组 array、函数 function、整型 int、或其他的基本类型。type 和 kind 是区别不是那么容易理清楚，但是可以这么想：\n当你定义一个名称为 Foo 的结构体，那么它的 kind 是 struct，而它的 type 是 Foo。\n当使用反射时，我们必须要意识到：在使用 reflect 包时，会假设你清楚的知道自己在做什么，如果使用不当，将会产生 panic。举个例子，你在 int 类型上调用 struct 结构体类型上才用的方法，你的代码就会产生 panic。我们时刻要记住，什么类型有有什么方法可以使用，从而避免产生 panic。\n如果一个变量是指针、映射、切片、管道、或者数组类型，那么这个变量的类型就可以调用方法 varType.Elem()。\n如果一个变量是结构体，那么你就可以使用反射去得到它的字段个数，并且可以得到每个字段的信息，这些信息包含在 reflect.StructField 结构体中。reflect.StructField 包含字段的名称、排序、类型、标签。\n前言万语也不如一行代码看的明白，下面的这个例子输出了不同变量所属类型的信息。\ntype Foo struct { A int `tag1:\u0026#34;First Tag\u0026#34; tag2:\u0026#34;Second Tag\u0026#34;` B string } func main() { sl := []int{1, 2, 3} greeting := \u0026#34;hello\u0026#34; greetingPtr := \u0026amp;greeting f := Foo{A: 10, B: \u0026#34;Salutations\u0026#34;} fp := \u0026amp;f slType := reflect.TypeOf(sl) gType := reflect.TypeOf(greeting) grpType := reflect.TypeOf(greetingPtr) fType := reflect.TypeOf(f) fpType := reflect.TypeOf(fp) examiner(slType, 0) examiner(gType, 0) examiner(grpType, 0) examiner(fType, 0) examiner(fpType, 0) } func examiner(t reflect.Type, depth int) { fmt.Println(strings.Repeat(\u0026#34;\\t\u0026#34;, depth), \u0026#34;Type is\u0026#34;, t.Name(), \u0026#34;and kind is\u0026#34;, t.Kind()) switch t.Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: fmt.Println(strings.Repeat(\u0026#34;\\t\u0026#34;, depth+1), \u0026#34;Contained type:\u0026#34;) examiner(t.Elem(), depth+1) case reflect.Struct: for i := 0; i \u0026lt; t.NumField(); i++ { f := t.Field(i) fmt.Println(strings.Repeat(\u0026#34;\\t\u0026#34;, depth+1), \u0026#34;Field\u0026#34;, i+1, \u0026#34;name is\u0026#34;, f.Name, \u0026#34;type is\u0026#34;, f.Type.Name(), \u0026#34;and kind is\u0026#34;, f.Type.Kind()) if f.Tag != \u0026#34;\u0026#34; { fmt.Println(strings.Repeat(\u0026#34;\\t\u0026#34;, depth+2), \u0026#34;Tag is\u0026#34;, f.Tag) fmt.Println(strings.Repeat(\u0026#34;\\t\u0026#34;, depth+2), \u0026#34;tag1 is\u0026#34;, f.Tag.Get(\u0026#34;tag1\u0026#34;), \u0026#34;tag2 is\u0026#34;, f.Tag.Get(\u0026#34;tag2\u0026#34;)) } } } } 输出如下：\nType is and kind is slice Contained type: Type is int and kind is int Type is string and kind is string Type is and kind is ptr Contained type: Type is string and kind is string Type is Foo and kind is struct Field 1 name is A type is int and kind is int Tag is tag1:\u0026#34;First Tag\u0026#34; tag2:\u0026#34;Second Tag\u0026#34; tag1 is First Tag tag2 is Second Tag Field 2 name is B type is string and kind is string Type is and kind is ptr Contained type: Type is Foo and kind is struct Field 1 name is A type is int and kind is int Tag is tag1:\u0026#34;First Tag\u0026#34; tag2:\u0026#34;Second Tag\u0026#34; tag1 is First Tag tag2 is Second Tag Field 2 name is B type is string and kind is string 运行示例\n创建实例 # 除了检查变量的类型外，你还可以利用来获取、设置和创建变量。首先，通过 refVal := reflect.ValueOf(var) 创建类型为 reflect.Value 的实例。如果你想通过反射来更新值，那么必须要获取到变量的指针 refPtrVal := reflect.ValueOf(\u0026amp;var)，如果不这么做，那么你只能读取值，而不能设置值。\n一旦得到变量的 reflect.Value，你就可以通过 Value 的 Type 属性获取变量的 reflect.Type 类型信息。\n如果想更新值，记住要通过指针，而且在设置时，要先取消引用，通过 refPtrVal.Elem().Set(newRefVal) 更新其中的值，传递给 Set 的参数也必须要是 reflect.Value 类型。\n如果想创建一个新的变量，可以通过 reflect.New(varType) 实现，传递的参数是 reflect.Type 类型，该方法将会返回一个指针，如前面介绍的那样，你可以通过使用 Elem().Set() 来设置它的值。\n最终，通过 Interface() 方法，你就得到一个正常的变量。Go 中没有泛型，变量的类型将会丢失，Interface() 方法将会返回一个类型为 interface{} 的变量。如果你为了能更新值，创建的是一个指针，那么需要使用 Elem().Interface() 来获取变量。但无论是上面的哪种情况，你都需要把 interface{} 类型变量转化为实际的类型，如此才能使用。\n下面是一些代码，实现了这些概念。\ntype Foo struct { A int `tag1:\u0026#34;First Tag\u0026#34; tag2:\u0026#34;Second Tag\u0026#34;` B string } func main() { greeting := \u0026#34;hello\u0026#34; f := Foo{A: 10, B: \u0026#34;Salutations\u0026#34;} gVal := reflect.ValueOf(greeting) // not a pointer so all we can do is read it fmt.Println(gVal.Interface()) gpVal := reflect.ValueOf(\u0026amp;greeting) // it’s a pointer, so we can change it, and it changes the underlying variable gpVal.Elem().SetString(\u0026#34;goodbye\u0026#34;) fmt.Println(greeting) fType := reflect.TypeOf(f) fVal := reflect.New(fType) fVal.Elem().Field(0).SetInt(20) fVal.Elem().Field(1).SetString(\u0026#34;Greetings\u0026#34;) f2 := fVal.Elem().Interface().(Foo) fmt.Printf(\u0026#34;%+v, %d, %s\\n\u0026#34;, f2, f2.A, f2.B) } 输出如下：\nhello goodbye {A:20 B:Greetings}, 20, Greetings 运行示例\n无 make 的创建实例 # 对于像 slice、map、channel类型，它们需要用 make 创建实例，你也可以使用反射实现。slice 使用 reflect.MakeSlice，map 使用 reflect.MakeMap，channel 使用 reflect.MakeChan，你需要提供将创建变量的类型，即 reflect.Type，传递给这些函数。成功调用后，你将得到一个类型为 reflect.Value 的变量，你可以通过反射操作这个变量，操作完成后，就 可以将它转化为正常的变量。\nfunc main() { // declaring these vars, so I can make a reflect.Type intSlice := make([]int, 0) mapStringInt := make(map[string]int) // here are the reflect.Types sliceType := reflect.TypeOf(intSlice) mapType := reflect.TypeOf(mapStringInt) // and here are the new values that we are making intSliceReflect := reflect.MakeSlice(sliceType, 0, 0) mapReflect := reflect.MakeMap(mapType) // and here we are using them v := 10 rv := reflect.ValueOf(v) intSliceReflect = reflect.Append(intSliceReflect, rv) intSlice2 := intSliceReflect.Interface().([]int) fmt.Println(intSlice2) k := \u0026#34;hello\u0026#34; rk := reflect.ValueOf(k) mapReflect.SetMapIndex(rk, rv) mapStringInt2 := mapReflect.Interface().(map[string]int) fmt.Println(mapStringInt2) } 输出如下：\n[10] map[hello:10] 运行示例\n创建函数 # 你不仅经可以通过反射创建空间存储数据，还可以通过反射提供的函数 reflect.MakeFunc 来创建新的函数。这个函数期待接收参数有两个，一个是 reflect.Type 类型，并且 Kind 为 Function，另外一个是闭包函数，它的输入参数类型是 []reflect.Value，输出参数是 []reflect.Value。\n下面是一个快速体验示例，可为任何函数在外层包裹一个记录执行时间的函数。\nfunc MakeTimedFunction(f interface{}) interface{} { rf := reflect.TypeOf(f) if rf.Kind() != reflect.Func { panic(\u0026#34;expects a function\u0026#34;) } vf := reflect.ValueOf(f) wrapperF := reflect.MakeFunc(rf, func(in []reflect.Value) []reflect.Value { start := time.Now() out := vf.Call(in) end := time.Now() fmt.Printf(\u0026#34;calling %s took %v\\n\u0026#34;, runtime.FuncForPC(vf.Pointer()).Name(), end.Sub(start)) return out }) return wrapperF.Interface() } func timeMe() { fmt.Println(\u0026#34;starting\u0026#34;) time.Sleep(1 * time.Second) fmt.Println(\u0026#34;ending\u0026#34;) } func timeMeToo(a int) int { fmt.Println(\u0026#34;starting\u0026#34;) time.Sleep(time.Duration(a) * time.Second) result := a * 2 fmt.Println(\u0026#34;ending\u0026#34;) return result } func main() { timed := MakeTimedFunction(timeMe).(func()) timed() timedToo := MakeTimedFunction(timeMeToo).(func(int) int) fmt.Println(timedToo(2)) } 输出如下：\nstarting ending calling main.timeMe took 1s starting ending calling main.timeMeToo took 2s 4 运行示例\n创建一个新的结构 # Go 中，反射还可以在运行时创建一个全新的结构体，你可以通过传递一个 reflect.StructField 的 slice 给 reflect.StructOf 函数来实现。是不是听起来挺荒诞的，我们创建的一个新的类型，但是这个类型没有名字，因此也就无法将它转化为正常的变量。你可以通过它创建实例，用 Interface() 把它的值转给类型为 interface{} 的变量，但是如果要设置它的值，必须来反射来做。\nfunc MakeStruct(vals ...interface{}) interface{} { var sfs []reflect.StructField for k, v := range vals { t := reflect.TypeOf(v) sf := reflect.StructField{ Name: fmt.Sprintf(\u0026#34;F%d\u0026#34;, (k + 1)), Type: t, } sfs = append(sfs, sf) } st := reflect.StructOf(sfs) so := reflect.New(st) return so.Interface() } func main() { s := MakeStruct(0, \u0026#34;\u0026#34;, []int{}) // this returned a pointer to a struct with 3 fields: // an int, a string, and a slice of ints // but you can’t actually use any of these fields // directly in the code; you have to reflect them sr := reflect.ValueOf(s) // getting and setting the int field fmt.Println(sr.Elem().Field(0).Interface()) sr.Elem().Field(0).SetInt(20) fmt.Println(sr.Elem().Field(0).Interface()) // getting and setting the string field fmt.Println(sr.Elem().Field(1).Interface()) sr.Elem().Field(1).SetString(\u0026#34;reflect me\u0026#34;) fmt.Println(sr.Elem().Field(1).Interface()) // getting and setting the []int field fmt.Println(sr.Elem().Field(2).Interface()) v := []int{1, 2, 3} rv := reflect.ValueOf(v) sr.Elem().Field(2).Set(rv) fmt.Println(sr.Elem().Field(2).Interface()) } 输出如下：\n0 20 reflect me [] [1 2 3] 运行示例\n反射的限制 # 反射有一个大的限制。虽然运行时可以通过反射创建新的函数，但无法用反射创建新的方法，这也就意味着你不能在运行时用反射实现一个接口，用反射创建的结构体使用起来很支离破碎。而且，通过反射创建的结构体，无法实现 GO 的一个特性 —— 通过匿名字段实现委托模式。\n看一个通过结构体实现委托模式的例子，通常情况下，结构体的字段都会定义名称。在这例子中，我们定义了两个类型，Foo 和 Bar：\ntype Foo struct { A int } func (f Foo) Double() int { return f.A * 2 } type Bar struct { Foo B int } type Doubler interface { Double() int } func DoDouble(d Doubler) { fmt.Println(d.Double()) } func main() { f := Foo{10} b := Bar{Foo: f, B: 20} DoDouble(f) // passed in an instance of Foo; it meets the interface, so no surprise here DoDouble(b) // passed in an instance of Bar; it works! } 运行示例\n代码中显示，Bar 中的 Foo 字段并没有名称，这使它成了一个匿名或内嵌的字段。Bar 也是满足 Double 接口的，虽然只有 Foo 实现了 Double 方法，这种能力被称为委托。在编译时，Go 会自动为 Bar 生成 Foo 中的方法。这不是继承，如果你尝试给一个只接收 Foo 的函数传递 Bar，编译将不会通过。\n如果你用反射去创建一个内嵌字段，并且尝试去访问它的方法，将会产生一些非常奇怪的行为。最好的方式就是，我们不要用它。关于这个问题，可以看下 github 的两个 issue，issue/15924 和 issues/16522。不幸的是，它们还没有任何的进展。\n那么，这会有什么问题呢？如果支持动态的接口，我们可以实现什么功能？如前面介绍，我们能通过 Go 的反射创建函数，实现包裹函数，通过 interface 也可以实现。在 Java 中，这叫做动态代理。当把它和注解结合，将能得到一个非常强大的能力，实现从命令式编程方式到声明式编程的切换，一个例子 JDBI，这个 Java 库让你可以在 DAO 层定义一个接口，它的 SQL 查询通过注解定义。所有数据操作的代码都是在运行时动态生成，就是如此的强大。\n有什么意义 # 即使有这个限制，反射依然一个很强大的工具，每位 Go 开发者都应该掌握这项技能。但我们如何利用好它呢，下一篇，阅读 原版，再介绍，我将会通过一些库来探索反射的使用，并将利用它实现一些功能。\n我的博文：Go 中如何使用反射 Part One，译：Learning to Use Go Reflection\n","date":"2019-08-10","externalUrl":null,"permalink":"/posts/2019-08-10-reflection-in-golang/","section":"文章","summary":"什么是反射 # 多数情况下，Go 中的变量、类型和函数的使用都是非常简单的。\n当你需要一个类型：\n","title":"Go 中如何使用反射 Part One","type":"posts"},{"content":"继上篇 Go 问答汇总，已经过去了一个多月。今天汇总下近一个多月我关于 Go 的回答。\n粗略数了一下，一个多月的时间里，大约回答了 18 个与 Go 有关的问题，问题主要是来源于 segmentfault 和 zhihu 两个平台。后面希望加入更多平台，如 stackoverflow、github 的感兴趣主题。\n最近在写一个小工具，准备用于帮助自己回答不同平台的问题，同时也便于每个月的问题汇总。写的有点慢，希望月底可以完成。\n正文部分开始。\ngolang中如何将redis取出的map[string]string数据解析到目标struct中？\n主要和反射相关。\n问题主要是关于 map 中如果存在日期字符格式串，如何解析到 time.Time 类型成员中，而对于结构体而言，reflect.Kind() 返回的只能说明字段类型是 struct，并不能确定真正的类型，这时可以用 Go 的 switch type 类型查询语法实现。\n补充一点，在回答中没有提到的。\n在实现 map 到 struct 的通用方法时，我们比较容易想到支持基础类型，但对于结构体类型而言，可能性太多，如何更灵活地解决问题？我觉得，可通过钩子方式实现，即如果自定义类型需要支持 map 到 struct 的转化，可通过在自定义类型上增加钩子方法实现，比如 UnmarshalMap。如何实现可参考下 encoding/json。\n当然，这个工作已经有人做了，参考 github 上的包，mitchellh/mapstructure。前面说的 Hook 也是支持的。\ngolang 怎么优雅的实现错误码？\nGo 对错误处理有一套自己的理念。这个问题，我只是简单回答了一下，简单的思路，我定义了用户级别错误和系统级别错误。上篇问答汇总也会类似问题。\ngolang什么时候该返回error，什么时候panic？\n我的建议是，发生的 error 是否已经严重影响服务逻辑，如果在预判之内的错误，我们就应该 return error，记录日志，并不需要人工干预才能恢复，否则建议 panic。\n举个例子，在一般情况下，服务启动时，需要进行完善的初始化工作，确认各个组件的运行正常。如果初始化都失败了，那就没有必要继续向下走了，应该 panic 赶紧提示。\nGolang time 如何实现的？\n问题标题看着挺大，其实题主关心是几个核心常量之间的转化关系。主要是三个时间，分别 unix 时间、wall 时间和 absolute 时间。这里面有个相对重要的转化公式，在需要考虑平润年的时候稍微有点复杂。\n不多介绍了，具体自己看回答吧。\ngolang 中时候用指针什么时候用普通对象？\n其实就两点，一是如果数据结构比较大，建议采用指针，不会发生值拷贝。二是如果需要修改结构的话，必须用指针。当然如果是引用类型，比如 chan、slice、 map，就不用考虑这个问题了。\nGolang中的make(T, args)为什么返回T而不是*T?\nmake 针对的是 Go 的引用类型，即 chan、slice 和 map，而 new 针对的指针。引用类型为什么 make 不是返回指针呢？这样一说好像和上个问题有点类似了，当然因为指针不存在值类型的那些问题。\n在循环中 append map 到 map slice，map slice 中的数据全部为最后一次 append 的数据\n与上一个问题知识点类似，map 是引用类型，即使 slice 通过 append 赋值了多份 map 变量，但是其内部指向是同一个地址。\ngolang中哪些引用类型的指针在声明时不用加\u0026amp;号，哪些在函数定义的形参和返回值类型中不用*号标注\n与前面问题类似，具体看回答。\n我的理解，从这里向前数的四个问题，考察知识点基本类似，简单点说，就是 make 和 new 和问题。本质上讲，就是变量内部就是什么的问题。\n为什么 go语言的slice内部函数那么少？\n说实话，我也不明白为什么 Go 团队没有像为 string 类型那样为 slice 提供相应的标准库帮助 slice 更方便的操作。但其实，即使没有这样的包， slice 常见的各种增删改查也是可以实现，就是稍微有点 hack。具体如何实现，看看我的问答吧。\ngolang 等值比较是不是直接比较地址呢？\n首先要说 Go 的等值比较比较的是值，而不是地址。Go 中变量的可比较类型是内置的，基本所有类型都可以进行比较，包括 interface 和 struct。两个变量可比较的提前必须是相同类型。但有一点需要说明的是，interface 是不确定的类型，所有它不但会比较值，还有比较具体的类型。\n回答完这个问题后，我突然想起前段时间比较两个同类型结构体时还用了反射包中 reflect.DeepEqual 方法，真的是浪费资源啊。\ngolang 中如何禁止一个导出类型直接构造，必须通过new函数来构造？\n其他的 oo 语言实现题主要求是非常简单的，只要定义相应的私有成员属性并通过构造函数控制输入的参数即可。\n那么 Go 该如何实现呢？其实也很简单，思路与 oo 是类似的。只是我们把 oo 语言中的构造函数换成了 Go 中的工厂方法，私有变量变成了 Go 包级别的私有成员属性。我们只需要通过定义指定的可导出的工厂方法创建实例即可。\n入门，进阶GO语言，有什么好的书籍推荐？\n我从入门、中级到进阶三个阶段推荐了几本书。有兴趣的朋友，具体查看回答吧。\n如何评价 Go 标准库中新增的 plugin 包？\n这个问题的回答，我是先学先买的。看了 medium 中几篇关于 plugin 使用案例的文章，总共花了大概三四小时。plugin 包使 Go 是可以实现动态模块加载的能力，可以在不用重新编译主程序的情况下加入新功能。这是有一定的价值的。\n但 plugin 包也存在一些问题，使用起来会用一些限制因素。但如果我们清楚地了解，还是能拎的清我们应该在什么场景下使用它。具体有啥限制，查看回答吧。\ngo build 如何隐藏全局静态字符串变量？\n这个问题，我并没有找到啥好办法，能想到的就是通过加密解密的方式解决。当然，其实在真实的项目中，我们可以通过引入外部服务实现，比如 k8s 的配置加密功能，使用 ectd 管理配置等。\ngo语言中, 空的死循环与永远阻塞的chan细节上有什么差异?\nGoroutine 是抢占式的，这不同于传统的协程模型。但它又不是完全的抢占式，单核的情况下，还是需要 CPU 主动出入资源的，而空死循环将会一直占用着 CPU，对资源的浪费严重，而 chan 阻塞会出让 CPU 资源，实现并发执行。这应该是两者最大的不同吧。\n除了一般的 chan 实现阻塞，问答还介绍一些其他方式，也可以实现类似的效果。感兴趣的朋友可查看回答。\nGolang中fmt.Println和直接println有什么区别？\nprintln 主要是 Go 自己使用，比如源码、标准库等，而 fmt 才是给 Go 开发人员使用的。而且要提的是 println 不能保证兼容性，可能在未来的某一天就不存在了，但 fmt 中的函数就不存在着这样的问题。\n当然，两者的使用和效果上也是有区别的，如 println 输出是到标准错误的，而非标准输出。\n如何阅读Golang的源码？\n这个回答是个大工程，零零碎碎花了我差不多三个星期的时间。什么原因呢？\n我提了一个阅读源码的思路，分成了大概三步，大致分别是，了解使用、熟悉架构和剖析原理。我最近在思考是否自己也开始剖析源码，于是决定先以第一步入手，把 Go 的源码整个撸一篇，大概了解涉及了的内容和它们的用途。总共涉及四十多个部分，于是花的时间就比较长。\n第一步，了解使用这一块，有些部分达到了解使用，但是有些部分只是达到了解的层次。如果要完整阅读源码，很难，但大概阅读还是可行的。准备等等合适时机启动系统的源码阅读的计划。\ngolang数据库操作的时候，需要go func()吗？跟python异步操作yield有什么不同？\n上篇 Go 问答汇总篇 也有类似问题。我的理解，使用 go func 的前提是必须有可并行执行的任务，这是一个重要前提。\n很多时候，大家学 Go 都是冲着 Go 的并发来的，结果学之后发现压根用不上，很郁闷啊，总觉得哪里用的不对，不应该是这样的。一般的业务开发，特别是 web 的业务开发，使用并发的场景确实不多。而并发性能的问题，服务框架已经帮助我们实现了，完全没有插手机会，要想真正学会并发，不能只是每天的增删改查。\n汇总完毕！回答如有错误，欢迎大家指正。最后，感谢阅读，汇总篇似乎是比较枯燥的！\n","date":"2019-08-10","externalUrl":null,"permalink":"/posts/2019-08-10-zhihu-go-part2/","section":"文章","summary":"继上篇 Go 问答汇总，已经过去了一个多月。今天汇总下近一个多月我关于 Go 的回答。\n粗略数了一下，一个多月的时间里，大约回答了 18 个与 Go 有关的问题，问题主要是来源于 segmentfault 和 zhihu 两个平台。后面希望加入更多平台，如 stackoverflow、github 的感兴趣主题。\n","title":"Go 问答汇总 Part Two","type":"posts"},{"content":" 译者前言 # 最近发现我的翻译是越来越随性了，刚开始文章翻译的时候比较拘束，现在更多强调可读性，比如有些对文章大意没有什么影响的文字我现在都会选择直接跳过。\n这篇文章主要是关于 INI 解释器的 parser 实现，它会从上一节中 Lexer 中接收 Token 解析，最终返回给使用者具有实际意义的结构体。读了这个系列的文章，我相信大家对词法器实现的原理将会有了基本的理解，但如果要真正实践，似乎还有一段距离。有兴趣的话，我们可以实现个自己的 JSON 解释器。要求可以稍微简化，只解析到 JSON 的第一层。\n译文如下：\n本系列第一篇文章，英文原版，我们介绍了词法分析解析的一些基础概念，了解了 INI 文件的基本组成，并在此基础上定义了一些常量和结构体，这对我们接下来实现 INI 文件解析会很有帮助。\n第二篇文章，英文原版，因主要聚焦在 Lexer 的实现。它完成了将输入文本转化为 Token 的过程。\n今天是本系列的最后一篇文章，最终完成我们的解释器。解释器负责从 channel 读取 Token，并最终创建表示 INI 文件内容的结构体实例。解析完成后，我们可以用 JSON 格式将结果打印出来。\n结构体 # 解析器负责启动词法器和从 channel 读取 Token 的组件。接收到 Token 后，解析器需要知道当前 Token 状态，然后将其解析到对应结构中。我们要做的第一件事就是，定义表示 INI 内容的结构体。将主要涉及三个结构体。\n第一个表示 Key/Value 的结构体，名称为 IniKeyValue，如下。\nmodel/ini/IniKeyValue.go\npackage ini type IniKeyValue struct { Key string `json:\u0026#34;key\u0026#34;` Value string `json:\u0026#34;value\u0026#34;` } 第二个表示 Section 的结构体，名称为 IniSection，如下：\n/model/ini/IniSection.go\npackage ini type IniSection struct { Name string `json:\u0026#34;name\u0026#34;` KeyValuePairs []IniKeyValue `json:\u0026#34;keyValuePairs\u0026#34;` } 我们知道，Section 是由 Key/Value 组成的，其中 KeyValuePairs 即是属于这个 Section 的 Key/Value。如果 Key/Value 是不属于任何 Section，将会属于 Name 为空的 Section 中。\n最后一个表示整个文件的结构体，名称为 IniFile，如下：\n/model/ini/IniFile.go\npackage ini type IniFile struct { FileName string `json:\u0026#34;fileName\u0026#34;` Sections []IniSection `json:\u0026#34;sections\u0026#34;` } IniFile 有两个成员字段组成，分别 FileName 文件名称和一系列 Section。\n解析器 # 解析器的编写，我们要做的第一件事是，创建一个用于存放解析结构的变量，即一个 IniFile 结构体类型变量。如下：\noutput := ini.IniFile{ FileName: fileName, Sections: make([]ini.IniSection, 0), } 现在，我们还需要一些变量追踪 Token、 Token 的值，还有解析器当前的状态。例如，当我们在获取 Key/Value 时，我们需要知道当前处于哪个 Section 中。接下来，就可以启动词法器了。\nvar token lexertoken.Token var tokenValue string /* State variables */ key := \u0026#34;\u0026#34; log.Println(\u0026#34;Starting lexer and parser for file\u0026#34;, fileName, \u0026#34;...\u0026#34;) l := lexer.BeginLexing(filename, input) 到此，相关变量已经定义完成，并且词法器也成功启动。接下来开始从 channel 接收 Token，如果 Token 类型不是 TOKEN_VALUE，执行 Trim 空格。\nfor { token = l.NextToken() if token.Type != lexertoken.TOKEN_VALUE { tokenValue = strings.TrimSpace(token.Value) } else { tokenValue = token.Value } ... 我们知道，如果词法器遍历到文件结尾，将返回类型为 EOF 的 Token。此时，需要将 Section 和 Key/Value 记录下来，并退出循环。\nif isEOF(token) { output.Sections = append(output.Sections, section) break } parser 函数的最后实现具体 Token 的处理，主要有三个 Token 需要关注。\n第一个是 Section Token，如果遇到 Section Token，我们首先检查 section 变量中是否已存在 Key/Value，有则将它记录到 output.Sections 中。然后重置 section 变量追踪当前 Section 和接下来 Key/Value。\n接着是 Key/Value。如果遇到 TOKEN_KEY，变量 key 用于记录 TOKEN_KEY 的值。遇到 TOKEN_VALUE，我们就可以变量 key 对应的 value，并将其 append 到 section.KeyValuePairs 中。\n示例代码如下：\nswitch token.Type { case lexertoken.TOKEN_SECTION: /* * Reset tracking variables */ if len(section.KeyValuePairs) \u0026gt; 0 { output.Sections = append(output.Sections, section) } key = \u0026#34;\u0026#34; section.Name = tokenValue section.KeyValuePairs = make([]ini.IniKeyValue, 0) case lexertoken.TOKEN_KEY: key = tokenValue case lexertoken.TOKEN_VALUE: section.KeyValuePairs = append(section.KeyValuePairs, ini.IniKeyValue{ Key: key, Value: tokenValue, }) key = \u0026#34;\u0026#34; } 测试 # 开发工作已经完成，下面进入测试阶段。Github 上有相应的测试代码，go get 下载好代码，在你的 GOPATH 的 src/github.com/adampresley/sample-ini-parser 目录下的 sampleIniParser.go 即是测试代码。\n代码如下：\nsampleInput := ` key=abcdefg [User] userName=adampresley keyFile=~/path/to/keyfile [Servers] server1=localhost:8080 ` parsedINIFile := parser.Parse(\u0026#34;sample.ini\u0026#34;, sampleInput) prettyJSON, err := json.MarshalIndent(parsedINIFile, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) if err != nil { log.Println(\u0026#34;Error marshalling JSON:\u0026#34;, err.Error()) return } log.Println(string(prettyJSON)) 直接通过 go run sampleIniParser.go 执行即可。执行输出如下：\n2019/08/01 00:06:33 Starting lexer and parser for file sample.ini ... 2019/08/01 00:06:33 Parser has been shutdown 2019/08/01 00:06:33 { \u0026#34;fileName\u0026#34;: \u0026#34;sample.ini\u0026#34;, \u0026#34;sections\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;keyValuePairs\u0026#34;: [ { \u0026#34;key\u0026#34;: \u0026#34;key\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;abcdefg\u0026#34; } ] }, { \u0026#34;name\u0026#34;: \u0026#34;User\u0026#34;, \u0026#34;keyValuePairs\u0026#34;: [ { \u0026#34;key\u0026#34;: \u0026#34;userName\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;adampresley\u0026#34; }, { \u0026#34;key\u0026#34;: \u0026#34;keyFile\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;~/path/to/keyfile\u0026#34; } ] }, { \u0026#34;name\u0026#34;: \u0026#34;Servers\u0026#34;, \u0026#34;keyValuePairs\u0026#34;: [ { \u0026#34;key\u0026#34;: \u0026#34;server1\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;localhost:8080\u0026#34; } ] } ] } 总结 # 这个系列文章是非常有挑战性，但也非常有趣。词法分析与解析是一个非常复杂的话题，有太多内容需要学习。我们可以看到，即使像上面 INI 文件解析这样简单的工作，我们也需要花费一些精力才能完成。\n我的博文：，译：Writing a Lexer and Parser in Go - Part 3\n","date":"2019-07-29","externalUrl":null,"permalink":"/posts/2019-07-29-golang-lexer-and-parser-part3/","section":"文章","summary":"译者前言 # 最近发现我的翻译是越来越随性了，刚开始文章翻译的时候比较拘束，现在更多强调可读性，比如有些对文章大意没有什么影响的文字我现在都会选择直接跳过。\n","title":"Go 实现词法分析与解析 Part Three","type":"posts"},{"content":"平时比较喜欢逛逛问答平台，比如 stackvoerflow，最近想聚合下一些平台的技术问答，比如 stackoverflow。\n要完成这个工作，肯定是离不开爬虫工具。于是，我就想着顺便抽时间研究了 Go 的一款爬虫框架 colly。\n概要介绍 # colly 是 Go 实现的比较有名的一款爬虫框架，而且 Go 在高并发和分布式场景的优势也正是爬虫技术所需要的。它的主要特点是轻量、快速，设计非常优雅，并且分布式的支持也非常简单，易于扩展。\n本文将基于 colly 的官方文档，介绍 colly 的学习指南，以及我对 colly 的理解。\n如何学习 # 爬虫最有名的框架应该就是 Python 的 scrapy，很多人最早接触的爬虫框架就是它，我也不例外。它的文档非常齐全，扩展组件也很丰富。当我们要设计一款爬虫框架时，常会参考它的设计。之前看到一些文章介绍 Go 中也有类似 scrapy 的实现。\n相比而言，colly 的学习资料就少的可怜了。刚看到它的时候，我总会情不自禁想借鉴我的 scrapy 使用经验，但结果发现这种生搬硬套并不可行。\n到此，我们自然地想到去找些文章阅读，但结果是 colly 相关文章确实有点少，能找到的基本都是官方提供的，而且看起来似乎不是那么完善。没办法，慢慢啃吧！官方的学习资料通常都会有三处，分别是文档、案例和源码。\n今天，暂时先从官方文档角度吧！正文开始。\n官方文档 # 官方文档介绍着重使用方法，如果是有爬虫经验的朋友，扫完一遍文档很快。我花了点时间将官网文档的按自己的思路整理了一版。\n主体内容不多，涉及安装、快速开始、如何配置、调试、分布式爬虫、存储、运用多收集器、配置优化、扩展。\n其中的每篇文档都很短小，甚至是少的基本都不用翻页滚动。\n如何安装 # colly 的安装和其他的 Go 库安装一样简单。如下：\ngo get -u github.com/gocolly/colly 一行命令搞定。So easy!\n快速开始 # 我们来通过一个 hello word 案例快速体验下 colly 的使用。步骤如下：\n第一步，导入 colly。\nimport \u0026#34;github.com/gocolly/colly\u0026#34; 第二步，创建 collector。\nc := colly.NewCollector() 第三步，事件监听，通过 callback 执行事件处理。\n// Find and visit all links c.OnHTML(\u0026#34;a[href]\u0026#34;, func(e *colly.HTMLElement) { link := e.Attr(\u0026#34;href\u0026#34;) // Print link fmt.Printf(\u0026#34;Link found: %q -\u0026gt; %s\\n\u0026#34;, e.Text, link) // Visit link found on page // Only those links are visited which are in AllowedDomains c.Visit(e.Request.AbsoluteURL(link)) }) c.OnRequest(func(r *colly.Request) { fmt.Println(\u0026#34;Visiting\u0026#34;, r.URL) }) 我们顺便列举一下 colly 支持的事件类型，如下：\nOnRequest 请求执行之前调用 OnResponse 响应返回之后调用 OnHTML 监听执行 selector OnXML 监听执行 selector OnHTMLDetach，取消监听，参数为 selector 字符串 OnXMLDetach，取消监听，参数为 selector 字符串 OnScraped，完成抓取后执行，完成所有工作后执行 OnError，错误回调 最后一步，c.Visit() 正式启动网页访问。\nc.Visit(\u0026#34;http://go-colly.org/\u0026#34;) 案例的完整代码在 colly 源码的 _example 目录下 basic 中提供。\n如何配置 # colly 是一款配置灵活的框架，提供了大量的可供开发人员配置的选项。默认情况下，每个选项都提供了较优的默认值。\n如下是采用默认创建的 collector。\nc := colly.NewCollector() 配置创建的 collector，比如设置 useragent 和允许重复访问。代码如下：\nc2 := colly.NewCollector( colly.UserAgent(\u0026#34;xy\u0026#34;), colly.AllowURLRevisit(), ) 我们也可以创建后再改变配置。\nc2 := colly.NewCollector() c2.UserAgent = \u0026#34;xy\u0026#34; c2.AllowURLRevisit = true collector 的配置可以在爬虫执行到任何阶段改变。一个经典的例子，通过随机改变 user-agent，可以帮助我们实现简单的反爬。\nconst letterBytes = \u0026#34;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\u0026#34; func RandomString() string { b := make([]byte, rand.Intn(10)+10) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) } c := colly.NewCollector() c.OnRequest(func(r *colly.Request) { r.Headers.Set(\u0026#34;User-Agent\u0026#34;, RandomString()) }) 前面说过，collector 默认已经为我们选择了较优的配置，其实它们也可以通过环境变量改变。这样，我们就可以不用为了改变配置，每次都得重新编译了。环境变量配置是在 collector 初始化时生效，正式启动后，配置是可以被覆盖的。\n支持的配置项，如下：\nALLOWED_DOMAINS (字符串切片)，允许的域名，比如 []string{\u0026#34;segmentfault.com\u0026#34;, \u0026#34;zhihu.com\u0026#34;} CACHE_DIR (string) 缓存目录 DETECT_CHARSET (y/n) 是否检测响应编码 DISABLE_COOKIES (y/n) 禁止 cookies DISALLOWED_DOMAINS (字符串切片)，禁止的域名，同 ALLOWED_DOMAINS 类型 IGNORE_ROBOTSTXT (y/n) 是否忽略 ROBOTS 协议 MAX_BODY_SIZE (int) 响应最大 MAX_DEPTH (int - 0 means infinite) 访问深度 PARSE_HTTP_ERROR_RESPONSE (y/n) 解析 HTTP 响应错误 USER_AGENT (string) 它们都是些非常容易理解的选项。\n我们再来看看 HTTP 的配置，都是些常用的配置，比如代理、各种超时时间等。\nc := colly.NewCollector() c.WithTransport(\u0026amp;http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (\u0026amp;net.Dialer{ Timeout: 30 * time.Second, // 超时时间 KeepAlive: 30 * time.Second, // keepAlive 超时时间 DualStack: true, }).DialContext, MaxIdleConns: 100, // 最大空闲连接数 IdleConnTimeout: 90 * time.Second, // 空闲连接超时 TLSHandshakeTimeout: 10 * time.Second, // TLS 握手超时 ExpectContinueTimeout: 1 * time.Second, } 调试 # 在用 scrapy 的时候，它提供了非常好用的 shell 帮助我们非常方便地实现 debug。但非常可惜 colly 中并没有类似功能，这里的 debugger 主要是指运行时的信息收集。\ndebugger 是一个接口，我们只要实现它其中的两个方法，就可完成运行时信息的收集。\ntype Debugger interface { // Init initializes the backend Init() error // Event receives a new collector event. Event(e *Event) } 源码中有个典型的案例，LogDebugger。我们只需提供相应的 io.Writer 类型变量，具体如何使用呢？\n一个案例，如下：\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/gocolly/colly\u0026#34; \u0026#34;github.com/gocolly/colly/debug\u0026#34; ) func main() { writer, err := os.OpenFile(\u0026#34;collector.log\u0026#34;, os.O_RDWR|os.O_CREATE, 0666) if err != nil { panic(err) } c := colly.NewCollector(colly.Debugger(\u0026amp;debug.LogDebugger{Output: writer}), colly.MaxDepth(2)) c.OnHTML(\u0026#34;a[href]\u0026#34;, func(e *colly.HTMLElement) { if err := e.Request.Visit(e.Attr(\u0026#34;href\u0026#34;)); err != nil { log.Printf(\u0026#34;visit err: %v\u0026#34;, err) } }) if err := c.Visit(\u0026#34;http://go-colly.org/\u0026#34;); err != nil { panic(err) } } 运行完成，打开 collector.log 即可查看输出内容。\n分布式 # 分布式爬虫，可以从几个层面考虑，分别是代理层面、执行层面和存储层面。\n代理层面 # 通过设置代理池，我们可以将下载任务分配给不同节点执行，有助于提供爬虫的网页下载速度。同时，这样还能有效降低因爬取速度太快而导致IP 被禁的可能性。\ncolly 实现代理 IP 的代码如下：\npackage main import ( \u0026#34;github.com/gocolly/colly\u0026#34; \u0026#34;github.com/gocolly/colly/proxy\u0026#34; ) func main() { c := colly.NewCollector() if p, err := proxy.RoundRobinProxySwitcher( \u0026#34;socks5://127.0.0.1:1337\u0026#34;, \u0026#34;socks5://127.0.0.1:1338\u0026#34;, \u0026#34;http://127.0.0.1:8080\u0026#34;, ); err == nil { c.SetProxyFunc(p) } // ... } proxy.RoundRobinProxySwitcher 是 colly 内置的通过轮询方式实现代理切换的函数。当然，我们也可以完全自定义。\n比如，一个代理随机切换的案例，如下：\nvar proxies []*url.URL = []*url.URL{ \u0026amp;url.URL{Host: \u0026#34;127.0.0.1:8080\u0026#34;}, \u0026amp;url.URL{Host: \u0026#34;127.0.0.1:8081\u0026#34;}, } func randomProxySwitcher(_ *http.Request) (*url.URL, error) { return proxies[random.Intn(len(proxies))], nil } // ... c.SetProxyFunc(randomProxySwitcher) 不过需要注意，此时的爬虫仍然是中心化的，任务只在一个节点上执行。\n执行层面 # 这种方式通过将任务分配给不同的节点执行，实现真正意义的分布式。\n如果实现分布式执行，首先需要面对一个问题，如何将任务分配给不同的节点，实现不同任务节点之间的协同工作呢？\n首先，我们选择合适的通信方案。常见的通信协议有 HTTP、TCP，一种无状态的文本协议、一个是面向连接的协议。除此之外，还可选择的有种类丰富的 RPC 协议，比如 Jsonrpc、facebook 的 thrift、google 的 grpc 等。\n文档提供了一个 HTTP 服务示例代码，负责接收请求与任务执行。如下：\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;github.com/gocolly/colly\u0026#34; ) type pageInfo struct { StatusCode int Links map[string]int } func handler(w http.ResponseWriter, r *http.Request) { URL := r.URL.Query().Get(\u0026#34;url\u0026#34;) if URL == \u0026#34;\u0026#34; { log.Println(\u0026#34;missing URL argument\u0026#34;) return } log.Println(\u0026#34;visiting\u0026#34;, URL) c := colly.NewCollector() p := \u0026amp;pageInfo{Links: make(map[string]int)} // count links c.OnHTML(\u0026#34;a[href]\u0026#34;, func(e *colly.HTMLElement) { link := e.Request.AbsoluteURL(e.Attr(\u0026#34;href\u0026#34;)) if link != \u0026#34;\u0026#34; { p.Links[link]++ } }) // extract status code c.OnResponse(func(r *colly.Response) { log.Println(\u0026#34;response received\u0026#34;, r.StatusCode) p.StatusCode = r.StatusCode }) c.OnError(func(r *colly.Response, err error) { log.Println(\u0026#34;error:\u0026#34;, r.StatusCode, err) p.StatusCode = r.StatusCode }) c.Visit(URL) // dump results b, err := json.Marshal(p) if err != nil { log.Println(\u0026#34;failed to serialize response:\u0026#34;, err) return } w.Header().Add(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) w.Write(b) } func main() { // example usage: curl -s \u0026#39;http://127.0.0.1:7171/?url=http://go-colly.org/\u0026#39; addr := \u0026#34;:7171\u0026#34; http.HandleFunc(\u0026#34;/\u0026#34;, handler) log.Println(\u0026#34;listening on\u0026#34;, addr) log.Fatal(http.ListenAndServe(addr, nil)) } 这里并没有提供调度器的代码，不过实现不算复杂。任务完成后，服务会将相应的链接返回给调度器，调度器负责将新的任务发送给工作节点继续执行。\n如果需要根据节点负载情况决定任务执行节点，还需要服务提供监控 API 获取节点性能数据帮助调度器决策。\n存储层面 # 我们已经通过将任务分配到不同节点执行实现了分布式。但部分数据，比如 cookies、访问的 url 记录等，在节点之间需要共享。默认情况下，这些数据是保存内存中的，只能是每个 collector 独享一份数据。\n我们可以通过将数据保存至 redis、mongo 等存储中，实现节点间的数据共享。colly 支持在任何存储间切换，只要相应存储实现 colly/storage.Storage 接口中的方法。\n其实，colly 已经内置了部分 storage 的实现，查看 storage。下一节也会谈到这个话题。\n存储 # 前面刚提过这个话题，我们具体看看 colly 已经支持的 storage 有哪些吧。\nInMemoryStorage，即内存，colly 的默认存储，我们可以通过 collector.SetStorage() 替换。\nRedisStorage，或许是因为 redis 在分布式场景下使用更多，官方提供了使用案例。\n其他还有 Sqlite3Storage 和 MongoStorage。\n多收集器 # 我们前面演示的爬虫都是比较简单的，处理逻辑都很类似。如果是一个复杂的爬虫，我们可以通过创建不同的 collector 负责不同任务的处理。\n如何理解这段话呢？举个例子吧。\n如果大家写过一段时间爬虫，肯定遇到过父子页面抓取的问题，通常父页面的处理逻辑与子页面是不同的，并且通常父子页面间还有数据共享的需求。用过 scrapy 应该知道，scrapy 通过在 request 绑定回调函数实现不同页面的逻辑处理，而数据共享是通过在 request 上绑定数据实现将父页面数据传递给子页面。\n研究之后，我们发现 scrapy 的这种方式 colly 并不支持。那该怎么做？这就是我们要解决的问题。\n对于不同页面的处理逻辑，我们可以定义创建多个收集器，即 collector，不同 collector 负责处理不同的页面逻辑。\nc := colly.NewCollector( colly.UserAgent(\u0026#34;myUserAgent\u0026#34;), colly.AllowedDomains(\u0026#34;foo.com\u0026#34;, \u0026#34;bar.com\u0026#34;), ) // Custom User-Agent and allowed domains are cloned to c2 c2 := c.Clone() 通常情况下，父子页面的 collector 是相同的。上面的示例中，子页面的 collector c2 通过 clone，将父级 collector 的配置也都复制了下来。\n而父子页面之间的数据传递，可以通过 Context 实现在不同 collector 之间传递。注意这个 Context 只是 colly 实现的数据共享的结构，并非 Go 标准库中的 Context。\nc.OnResponse(func(r *colly.Response) { r.Ctx.Put(\u0026#34;Custom-header\u0026#34;, r.Headers.Get(\u0026#34;Custom-Header\u0026#34;)) c2.Request(\u0026#34;GET\u0026#34;, \u0026#34;https://foo.com/\u0026#34;, nil, r.Ctx, nil) }) 如此一来，我们在子页面中就可以通过 r.Ctx 获取到父级传入的数据了。关于这个场景，我们可以查看官方提供的案例 coursera_courses。\n配置优化 # colly 的默认配置针对是少量站点的优化配置。如果你是针对大量站点的抓取，还需要一些改进。\n持久化存储 # 默认情况下，colly 中的 cookies 和 url 是保存在内存中，我们要换成可持久化的存储。前面介绍过，colly 已经实现一些常用的可持久化的存储组件。\n启用异步加快任务执行 # colly 默认会阻塞等待请求执行完成，这将会导致等待执行任务数越来越大。我们可以通过设置 collector 的 Async 选项为 true 实现异步处理，从而避免这个问题。如果采用这种方式，记住增加 c.Wait()，否则程序会立刻退出。\n禁止或限制 KeepAlive 连接 # colly 默认开启 KeepAlive 增加爬虫的抓取速度。但是，这对打开的文件描述符有要求，对于长时间运行的任务，进程非常容易就能达到最大描述符的限制。\n禁止 HTTP 的 KeepAlive 的示例代码，如下。\nc := colly.NewCollector() c.WithTransport(\u0026amp;http.Transport{ DisableKeepAlives: true, }) 扩展 # colly 提供了一些扩展，主要与爬虫相关的常用功能，如 referer、random_user_agent、url_length_filter 等。源码路径在 colly/extensions/ 下。\n通过一个示例了解它们的使用方法，如下：\nimport ( \u0026#34;log\u0026#34; \u0026#34;github.com/gocolly/colly\u0026#34; \u0026#34;github.com/gocolly/colly/extensions\u0026#34; ) func main() { c := colly.NewCollector() visited := false extensions.RandomUserAgent(c) extensions.Referrer(c) c.OnResponse(func(r *colly.Response) { log.Println(string(r.Body)) if !visited { visited = true r.Request.Visit(\u0026#34;/get?q=2\u0026#34;) } }) c.Visit(\u0026#34;http://httpbin.org/get\u0026#34;) } 只需将 collector 传入扩展函数中即可。这么简单就搞定了啊。\n那么，我们能不能自己实现一个扩展呢？\n在使用 scrapy 的时候，我们如果要实现一个扩展需要提前了解不少概念，仔细阅读它的文档。但 colly 在文档中压根也并没有相关说明啊。肿么办呢？看样子只能看源码了。\n我们打开 referer 插件的源码，如下：\npackage extensions import ( \u0026#34;github.com/gocolly/colly\u0026#34; ) // Referer sets valid Referer HTTP header to requests. // Warning: this extension works only if you use Request.Visit // from callbacks instead of Collector.Visit. func Referer(c *colly.Collector) { c.OnResponse(func(r *colly.Response) { r.Ctx.Put(\u0026#34;_referer\u0026#34;, r.Request.URL.String()) }) c.OnRequest(func(r *colly.Request) { if ref := r.Ctx.Get(\u0026#34;_referer\u0026#34;); ref != \u0026#34;\u0026#34; { r.Headers.Set(\u0026#34;Referer\u0026#34;, ref) } }) } 在 collector 上增加一些事件回调就实现一个扩展。这么简单的源码，完全不用文档说明就可以实现一个自己的扩展了。 当然，如果仔细观察，我们会发现，其实它的思路和 scrapy 是类似的，都是通过扩展 request 和 response 的回调实现，而 colly 之所以如此简洁主要得益于它优雅的设计和 Go 简单的语法。\n总结 # 读完 colly 的官方文档会发现，虽然它的文档简陋无比，但应该介绍的内容基本上都涉及到了。如果有部分未涉及的内容，我也在本文之中做了相关的补充。之前在使用 Go 的 elastic 包时，同样也是文档少的可怜，但简单读下源码，就能立刻明白了该如何去使用它。\n或许这就是 Go 的大道至简吧。\n最后，如果大家在使用 colly 时遇到什么问题，官方的 example 绝对是最佳实践，建议可以抽时间一读。\n我的博文：Colly 从入门到不放弃指南。\n","date":"2019-07-25","externalUrl":null,"permalink":"/posts/2019-07-25-colly-from-zero-to-hero/","section":"文章","summary":"平时比较喜欢逛逛问答平台，比如 stackvoerflow，最近想聚合下一些平台的技术问答，比如 stackoverflow。\n要完成这个工作，肯定是离不开爬虫工具。于是，我就想着顺便抽时间研究了 Go 的一款爬虫框架 colly。\n","title":"Colly 从入门到不放弃指南","type":"posts"},{"content":"","date":"2019-07-25","externalUrl":null,"permalink":"/tags/crawler/","section":"Tags","summary":"","title":"Crawler","type":"tags"},{"content":"本文是关于词法器实现的具体介绍，如果在阅读时遇到困难，建议参考源码阅读，文中的代码片段为了介绍思路。如何解析会在下一篇介绍。\n最近简单看了下 Go 源码，在 src/go 目录下有几个模块，token、scanner 和 parser 应该就是 Go 词法相关实现的核心代码，打开 token 目录会发现其中的源码和上一节介绍的内容有诸多相似之处。\n由于最近并发任务比较多，不能以最快的速度更新。词法的相关内容，除了本系列，我把其他一些相关文章的链接都贴在下面，如果英文阅读功底不错，可自行阅读。\nA look at Go lexer/scanner packages\nRob Pike\u0026rsquo;s Functional Way\nHandwritten Parser \u0026amp; Lexers In Go\n译文如下：\n本系列的第一篇文章（英文原版）。\n我介绍了关于词法分析与解析的一些基本概念和 INI 文件内容的基本组成。之后，我们创建了部分相关结构体与常量，帮助实现接下来的 INI 文本解析器。\n本篇文章将实际深入到词法分析的细节。\n词法分析 (lexing)，指的是将输入文本转化为一系列 Token 的过程。Token 是比文本更小的单元，将它们组合在一起才可能产生有实际意义的内容，如程序、配置文件等。\n本系列文章中的 INI 文件，Token 包括左括号、右括号、SectionName、Key，Value 以及等于号。用正确的顺序组合它们，你就会有一个 INI 文件。词法器的职责是读取 INI 文件内容、分析创建 Token，以及通过 channel 将 Token 发送给解析器。\n词法分析器 # 为了实现文本到 Token 的转化，我们还需要追踪一些信息，比如文本内容，当前分析文本的位置，以及当前分析的 Token 的开始和结束位置。\n完成分析后，我们还要将 Token 发送给解析器，可以通过 channel 传递。\n我们还需要一个函数实现词法器状态的追踪。Rob Pike 的演讲中谈到利用函数追踪词法器当前和接下来期望的状态。简单而言，就是一个函数处理一个 Token，并返回下一个状态函数生成下一个期望 Token。下面，我就简单翻译为状态函数吧.\n举个例子吧！\nINI 中 Section 由三部分组成，分别是左括号、SectionName 以及右括号。第一个函数将会生成左括号类型的 Token，返回 SectionName 的状态函数，它会分析处理 SectionName 的相关逻辑，并返回处理右括号的状态函数。总的顺序是，左括号 -\u0026gt; section 名称 -\u0026gt; 右括号。\n百闻不如意见，具体看下词法器的结构吧。如下：\nLexer.go\ntype Lexer struct { Name string Input string // 输入文本 Tokens chan lexertoken.Token // 用于向词法分析器发送 Token 的 channel State LexFn // 上面提到的状态函数 Start int // token 的开始位置，结束位置可以通过 start + len(token) 获得 Pos int // 词法器处理文本位置，当确认 Token 结尾时，即相当于知道 Token 的 end position Width int } LexFn.go\ntype LexFn func(*Lexer) LexFn // 词法器状态函数的定义，返回下一个期望 Token 的分析函数。 上篇文章，我们已经定义了 Token 结构。LexFn，是用于处理 Token 的词法器状态函数类型。\n现在再为我们的额词法器增加一些能力。Lexer 是用于文本处理的，为了获取下一个 Token，我们为 Lexer 增加诸如读取 rune 字符串、跳过空格，和其他一些有用的方法。基本都是文本处理的一些简单方法。\n/* Puts a token onto the token channel. The value of this token is read from the input based on the current lexer position. */ func (this *Lexer) Emit(tokenType lexertoken.TokenType) { this.Tokens \u0026lt;- lexertoken.Token{Type: tokenType, Value: this.Input[this.Start:this.Pos]} this.start = this.Pos } /* Increment the position */ func (this *Lexer) Inc() { this.Pos++ if this.Pos \u0026gt;= utf8.RuneCountInString(this.Input) { this.Emit(lexertoken.TOKEN_EOF) } } /* Return a slice of the input from the current lexer position to the end of the input string. */ func (this *Lexer) InputToEnd() string { return this.Input[this.Post:] } /* Skips whitespace until we get something meaningful */ func (this *Lexer) SkipWhiteSpace() { for { ch := this.Next() if !unicode.IsSpace(ch) { this.Dec() break } if ch == lexertoken.EOF { this.Emit(lexertoken.TOKEN_EOF) break } } } 重点需要了解的是，Token 的读取与发送。主要涉及几个步骤，如下：\n首先，一直读取字符，直到形成一个确定的 Token，举例说明，SectionName 的状态函数，只有读到右括号才能确认 SectionName。 接着，将 Token 和 Token 类型通过 channel 发送给解析器。 最后，判断下一个期望的状态函数，并返回。\n我们先定义一个启动函数。它同样是解析器（下篇文章）的启动入口。它初始化了一个 Lexer，赋予它第一个状态函数。\n第一个期望的 Token 可能是什么？一个特殊符号还是一个关键词？\n在我们的例子中，第一个状态函数将会用一个通用的名称 LexBegin 命名，因为在 INI 文件中，section 开始可以，但也可以没有 section，以 key/value 开投。LexBegin 会负责处理这个逻辑。\n/* Start a new lexer with a given input string. This returns the instance of the lexer and a channel of tokens. Reading this stream is the way to parse a given input and perform processing. */ func BeginLexing(name, input string) *lexer.Lexer { l := \u0026amp;lexer.Lexer{ Name: name, Input: input, State: lexer.LexBegin, Tokens: make(chan lexertoken.Token, 3), } return l } 开始 # 第一个状态函数 LexBegin。\n/* This lexer function starts everything off. It determines if we are beginning with a key/value assignment or a section. */ func LexBegin(lexer *Lexer) LexFn { lexer.SkipWhitespace() if strings.HasPrefix(lexer.InputToEnd(), lexertoken.LEFT_BRACKET) { return LexLeftBracket } else { return LexKey } } 正如所见，首先是跳过所有空格，INI 文件中，空格是没有意义。接着，我们需要确认第一个字符是否是左括号，是的话，则返回 LexLetBracket，否则即是 key 类型，返回 LexKey 状态函数。\nSection # 开始 section 的处理逻辑介绍。\nINI 文件中的 SectionName 是由左右括号包裹起来的。我们可以将 Key/Value 组织在某个 Section 中。在 LexBegin 中，如果发现了左括号，则会返回 LexLeftBracket 函数。\nLexLeftBracket 的代码如下：\n/* This lexer function emits a TOKEN_LEFT_BRACKET then returns the lexer for a section header. */ func LexLeftBracket(lexer *Lexer) LexFn { lexer.Pos += len(lexertoken.LEFT_BRACKET) lexer.Emit(lexertoken.TOKEN_LEFT_BRACKET) return LexSection } 代码很简单！根据括号长度（长度位 1），将词法器的位置后移，接着向 channel 发送 TOKEN_LEFT_BRACKET。\n在这个场景下，Token 内容并没有什么意义。当 Emit 执行完成后，开始位置被赋值为词法器当前位置，这将会为下一个 Token 做好准备。最后，返回用于处理 SectioName 的状态函数，LexSection。\n/* This lexer function exits a TOKEN_SECTION with the name of an INI file section header. */ func LexSection(lexer *Lexer) LexFn { for { if lexer.IsEOF() { return lexer.Errorf(errors.LEXER_ERROR_MISSING_RIGHT_BRACKET) } if strings.HasPrefix(lexer.InputEnd(), lexertoken.RIGHT_BRACKET) { lexer.Emit(lexertoken.TOKEN_SECTION) return LexRightBracket } lexer.Inc() } } 逻辑稍微有点复杂，但基本逻辑一样。\n函数中通过一个循环遍历字符，直到遇到 RIGHT_BRACKET，即右括号，才可以确认 SectionName 的结束位置。如果遇到 EOF，则说明是一个错误格式的 INI，我们应该进行错误提示，并通过 channel 发送给解析器。如果正常，将一直循环，直到发现右括号，然后 TOKEN_SECTION 和相应文本发送出去。\nLexSection 返回的状态函数是 LexerRightBracket，逻辑与 LexerLeftBracket 类似，不同的是，它返回的状态函数是 LexBegin， 原因是 Section 可能是空 Section，也可能有 Key/Value。\n/* This lexer function emits a TOKEN_RIGHT_BRACKET then returns the lexer for a begin. */ func LexRightBracket(lexer *Lexer) LexFn { lexer.Pos += len(lexertoken.RIGHT_BRACKET) lexer.Emit(lexertoken.TOKEN_RIGHT_BRACKET) return LexBegin } Key/Value # 继续 Key/Value 处理的介绍，它的表达形式非常简单：key=value。\n首先是 Key 的处理，和 LexSection 类似，一直循环直到遇到等于号才能确定一个完整的 Key。然后执行 Emit 将 Key 发送，并返回状态函数 LexEqualSign。\n/* This lexer function emits a TOKEN_KEY with the name of an key that will assigned a value */ func LexKey(lexer *Lexer) LexFn { for { if strings.HasPrefix(lexer.InputToEnd(), lexertoken.EQUAL_SIGN) { lexer.Emit(lexertoken.TOKEN_KEY) return LexEqualSign } lexer.Inc() if lexer.IsEOF() { return lexer.Errorf(errors.LEXER_ERROR_UNEXPECTED_EOF) } } } 等号的处理非常简单，和左右括号类似。直接发送 TOKEN_EQUAL_SIGN 类型 Token 给解析器，并返回 LexValue。\n/* This lexer functions emits a TOKEN_EQUAL_SIGN then returns the lexer for value. */ func LexEqualSign(lexer *Lexer) LexFn { lexer.Pos += len(lexertoken.EQUAL_SIGN) lexer.Emit(lexertoken.EQUAL_SIGN) return LexValue } 最后介绍的状态函数是 LexValue，用于 Key/Value 中的 Value 部分的处理。它会在遇到换行符时确认一个完整的Value。它返回的状态函数是 LexBegin，以此继续下一轮的分析。\n/* This lexer function emits a TOKEN_VALUE with the value to be assigned to a key. */ func LexValue(lexer *Lexer) LexFn { for { if strings.HasPrefix(lexer.InputToEnd(), lexertoken.NEWLINE) { lexer.Emit(lexertoken.TOKEN_VALUE) return LexBegin } lexer.Inc() if lexer.IsEOF() { return lexer.Errorf(errors.LEXER_ERROR_UNEXPECTED_EOF) } } } 接下来 # 在 Part 3，本系列的最后一篇，我们将会介绍如何创建一个基本的解析器，将从 lexer 获得的 Token 处理为我们期望得到的结构化数据。\n我的博文：Go 实现词法分析与解析 Part Two，译：Writeing a Lexer and Parser in Go - Part 2\n","date":"2019-07-24","externalUrl":null,"permalink":"/posts/2019-07-24-golang-lexer-and-parser-part2/","section":"文章","summary":"本文是关于词法器实现的具体介绍，如果在阅读时遇到困难，建议参考源码阅读，文中的代码片段为了介绍思路。如何解析会在下一篇介绍。\n最近简单看了下 Go 源码，在 src/go 目录下有几个模块，token、scanner 和 parser 应该就是 Go 词法相关实现的核心代码，打开 token 目录会发现其中的源码和上一节介绍的内容有诸多相似之处。\n","title":"Go 实现词法分析与解析 Part Two","type":"posts"},{"content":"最近比较忙，因为工作需要，必须快速了解一些新知识，写文少了，翻译多了。\n原因吗？也简单。\n翻译好文不仅可以帮助大家学习，自己也能学到更多。\n最近，单独开了个专栏，用于保存自己翻译的计算机相关译文，大家如果有兴趣可以关注一下。Go 专栏或许当写到一定的程度时，更新频率会下降，但译文应该不会。\n这周简单总结下近期在知乎上我的一些关于 Go 的问答。一方面是希望只关注专栏的朋友也能看到，毕竟不是所有内容都可以写出文章，另一方面，梳理一下也能方便以后自己查找。\n为什么Golang没有像Python中in一样的功能？ Go 中没有 in，为什么会这样呢？其实还是因为它比较简单，实现起来也不是很复杂。回答中介绍了三种关于在 Go 中实现 in 的方式。\ngolang中byte转int涉及到大小端问题吗？ 关于 Go 中大小端的问题，其实有专门的包处理这个问题，encoding/binary。回答中介绍了如何在 Go 中检查机器的大小端。还有，如何将 []byte 分别按大小端转化为 int 类型。\ngolang切片扩容时底层内存地址是连续的么,会不会出现不连续的情况？ 主要介绍了切片的底层结构，数组是连续，因而数据肯定是连续的。回答通过具体的代码测试了下 slice 是如何扩容的。\nGolang如何把json中的unicode编码转换成中文字符？ 在调试接口的时候，经常遇到 \\uxxxx 之类的字符串，为什么需要这样做呢？回答中说了一点个人的理解，\\uxxxx 本质是 ascii 码，可以不用在意客户端的编码。但是这种方式，在我理解，也存在缺点，回答有所介绍。\n为什么go的map数据竞争是fatal错误, 而不是panic? 题主要求挺高，要求从源码上去回答这个问题。为什么在发生数据竞争时，是 fatal error，而不是 panic。如果从设计上说，发生数据竞争是个比较严重的错误，会直接影响程序的执行结果，需要直接退出，而 panic 还是可以 recover 的，而这种错误还是不要 recover 比较好。当然，从源码层面，\n如何理解 Go 的接口\u0026quot; 对于传统面向对象语言的开发者，对于 Go 接口还是难以理解。最需要强调的一点，就是要先理解鸭子模型，基于此，再来理解为什么说 Go 接口是一组方法的集合，Go 接口是一种非侵入式的设计。\nGo 接口如何正确的运继承 关于第二个问题，确切的说，就是题主还没有正确的理解 Go 的接口，简单地利用以往经验来理解 Go，理解 Go 的继承。从某种意义上说，Go 没有语法层面的继承，要实现继承的效果，需要通过组合实现。\ngolang与C语言相比最重要的改进是什么？ 当别人问到 Go 相对 C 最重要的改进是什么的时候，我优先想到并不是 Go 的并发模型，而是接口。经过前面两个问题，也会发现 Go 的接口的有点。它让我们的编码变的非常灵活。\n为什么Go语言的Error Handling是一个败笔？ 关于 Go 的错误处理，这是个容易产生争论的问题，回答中我也只是引用了别人的言论，通过和传统的 try-catch 模型对比，我觉得也是一种更好的认识 Go 错误处理的一种方式。虽说，别人的说法不可全然接收，但多听听也总是好的。\n最近，读了不好关于 Go 错误处理的文章，果断时间翻译几篇出来。除此以外，希望能在系统写一篇关于 Go 错误处理的文章。\ngolang第三方库fasthttp为什么要使用slice而不是map来存储header 这个问题我的确想了很久，slice 能重用内存是很好理解的，但是一直没想明白，为什么 Add 函数那么写就实现了重用内存。在回答了题主的问题后，顺便简单比较了下 Go 中 map 和 slice 的区别，随便扯的，应该是不够系统的。\ngolang 这段代码全加go关键字为什么不能运行？ 在使用 Go 并发模型时，大家经常会认为 Go 的并发主要在函数前面加个 go 关键字就能加快效率。初期，我在学习的时候，也存在这个问题。本篇文章的题主也存在这个问题。\ngo语言解决并发的方法有哪些？ 不知道有多数人都认为，只要在 Go 使用并发模型就可以实现高性能了。题主也存在这个问题，没有并发场景，只有一个 IO 的情况，强行使用 Go 的并发以求提高性能，这是完全不可能的。\n我猜测，题主没有达到理想的效果，便认为 Go 中还有其他的并发方法来解决这个问题。看这个问题建议先具体看下问题描述。\n如何在golang http服务端程序中读取2次Request Body？ 问题大概是说 net/http 服务端的 req body 两次读取的第一次读取否需要立刻 close，回答中有个 stackoverflow 要求立刻关闭。而题主的问题是，为什么需要立刻关闭？\n这个问题，只能通过阅读源码解决，因为我搜了半天的资料都没有找到答案。这个回答有些曲折。最终结论是，题主的问题已经不是问题，因为这是只有 Go 1.5 以及之前的版本才存在的问题。\n想了解的朋友自行阅读回答。\n暂时就梳理这么多，如果发现还有其他的问答没有汇总，会继续补充进来。\n","date":"2019-07-22","externalUrl":null,"permalink":"/posts/2019-07-22-zhihu-go-part1/","section":"文章","summary":"最近比较忙，因为工作需要，必须快速了解一些新知识，写文少了，翻译多了。\n原因吗？也简单。\n翻译好文不仅可以帮助大家学习，自己也能学到更多。\n最近，单独开了个专栏，用于保存自己翻译的计算机相关译文，大家如果有兴趣可以关注一下。Go 专栏或许当写到一定的程度时，更新频率会下降，但译文应该不会。\n","title":"Go 问答汇总 Part One","type":"posts"},{"content":"一直对词法分析与解析的话题比较感兴趣，最近发现了好几篇相关的优秀文章，准备好好翻译和研究下。我的理解，词法分析与解析的应用还是比较广泛的，无论简单的配置文件、各种模板语言、还是我们每天在写编程语言都离不开它。\n本篇文章一个系列文章的第一篇，主要介绍的是词法分析与解析的一些基础概念，包括什么是词法分析，什么是解析，Token 如何表示等等。\n正文如下：\n从今天开始，我将会用三篇文章介绍在 Go 中如何构建一个简单的词法分析与解释器。文中介绍的内容主要是基于 Rob Pike 在 2011 年关于 Lexical Scanning In Go 的演讲。这个系列文章最终会包含一个功能完善的代码，它可用于 INI 类型文件的解析。\n三篇文章涉及内容分别是：\nGo 实现词法分析与解析，译：Writing a Lexer and Parser in Go - Part 1，如什么是词法分析、解析，以及案例的一部分介绍； Go 实现词法分析与解析)，译：Writing a Lexer and Parser in Go - Part 2； Go 实现词法分析与解析，译：Writing a Lexer and Parser in Go - Part 3； 概要 # 词法分析与解析是个比较复杂的话题，但这并不意味着我们无法一点点剖析和掌握它。为了帮助大家更好地了解它，接下来，我将会构建一个简单的 INI 文件解析器。这个解析器输入的是文本字符串，返回的是经过结构化处理的结果，结果包含多个 Section 和 Key/Value。我将用 Go 实现它。\n为什么选择 INI 文件？主要是因为它的简单性，结构容易理解。例如，下面就是一个简单的 INI 内容样例：\n[SetionName] key1=value 1 key2=value 2 样例中主要涉及了三个元素，充分理解它们对于我们如何设计 INI 解释器是非常有帮助的。\n段: Sections 键: Keys 值: Values Key/Value 属于 Section，每个 Section 可能不止一个 Key/Value，每个 INI 文件可以包含多个 Section。这是一种非常简单但非常高效的结构，特别适用于保存配置信息。\n上面的内容将会被解析为结构化数据，我们可以提前看下处理后的数据的 JSON 格式，如下：\n{ \u0026#34;FileName\u0026#34;: \u0026#34;sample.ini\u0026#34;, \u0026#34;Sections\u0026#34;: [ { \u0026#34;Name\u0026#34;: \u0026#34;SectionName\u0026#34;, \u0026#34;KeyValuePairs\u0026#34;: [ { \u0026#34;Key\u0026#34;: \u0026#34;key1\u0026#34;, \u0026#34;Value\u0026#34;: \u0026#34;value 1\u0026#34; }, { \u0026#34;Key\u0026#34;: \u0026#34;key2\u0026#34;, \u0026#34;Value\u0026#34;: \u0026#34;value 2\u0026#34; } ] } ] } 什么是词法分析 # 词法分析在 WIKI 中的定义是 \u0026ldquo;将字符串转化为一系列 Token 的过程，即，一系列有意义的字符串\u0026rdquo;。词法分析通常是在编译或运行之前执行。例如，PHP 是一种解释型语言，当你访问一个由 PHP 开发的站点，PHP 解释器将负责 PHP 代码的执行，并把生成的 HTML 返回给浏览器。PHP 代码先会经过词法分析得到一系列有意义的 Token。之后，PHP 解释器会按照这些 Token 执行接下来的操作，比如将 Token 结果缓存，以及执行具体工作等。\n什么是 Token # Token 是用于描述与归类从文本中分解出来的元素的一种结构。例如，之前的例子中，section 可归类为 TOKEN_SECTION，key 可以归类为 TOKEN_KEY。这种结构通常被用于追踪元素类类别和值。比如，前面的例子中，名为 SectionName 的 section，在 Token 中的结构是如下表示：\n{ \u0026#34;Type\u0026#34;: TOKEN_SECTION, \u0026#34;Value\u0026#34;: \u0026#34;SectionName\u0026#34; } 解析器、解释器或编译器将会根据得到 Token 决定如何执行、编译或生成代码/数据。\n什么是解析 # 词法分析器将输入文本拆分，并返回一系列结构化的 token。但 token 本身并没有什么价值，如此便引出了解析的概念。解析是指对 Tokens 进行语法分析的过程，它可以确保输入的文本的可用性和有意义的。\n例如，下面的样例就是非可用 INI 段 section。\n[SectionName]=Hi there 这段文本在经过词法分析后，将会得到一系列的 Token，它们将被用于 section、等于号和字符串的表示。这是词法分析的职责所在。而解析器则是决定它们是否有意义，即是否符合语法。对于 INI 格式而言，这些 Token 并不可用。我们实现的解析器将会从 channel 中接收 Token，创建相应的数据结构，包含 section 和 key/value。\n逐步拆解 # 本文最后一个任务，定义下面在词法分析器中将会使用 Token 类型结构，Token 的名称和相关的类型。首先是 Token 的结构，这个结构将会贯穿我们的整个代码，它将会通过 channel 传递给解析器。\n我们先来看下项目目录结构，可以查看 github 仓库, 在我的 Mac 上，目录结构是 ~/code/go/src/github.com/adampresley/sample-ini-parser，lexer 词法分析组件在 service/lexer 目录下。Token 结构的定义位于 ~/code/go/src/github.com/adampresley/sample-ini-parser/services/lexer/lexertoken 目录下。\npackage lexertoken import ( \u0026#34;fmt\u0026#34; ) type Token struct { Type TokenType Value string } 该结构清晰的表示一个 Token 由类型和值组成的结构。你可以已经注意到这里引用了一个还未定义的类型 TokenType。现在，我们来定义一下：\npackage lexertoken type TokenType int const ( TOKEN_ERROR TokenType = itoa TOKEN_EOF TOKEN_LEFT_BRACKET TOKEN_RIGHT_BRACKET TOKEN_EQUAL_SIGN TOKEN_NEWLINE TOKEN_SECTION TOKEN_KEY TOKEN_VALUE ) 我们新定义了一个名为 TokenType 的类型（源自整型），并创建了所有可能的 Token 类型常量，它们都是从 INI 文件基础上拆解而来。\n我们需要一种方式实现错误追踪，定义 TOKEN_ERROR 表示错误类型；\n当到达文本结尾，我们用 TOKEN_EOF 表示；\n段由左括号、文本、右括号三部分组成；\nTOKEN_LEFT_BRACKET TOKEN_SECTION TOKEN_RIGHT_BRACKET Key/Value，以等于号进行分隔；\nTOKEN_KEY TOKEN_EQUAL_SIGN TOKEN_VALUE Section 和 Key/Value 必须以换行符结尾，常量 TOKEN_NEWLINE；\n最后，我们还需要了解上面部分的 Token 类型的文本表示。比如，词法器在分析 Key/Value 是，会在它们之间寻找等于号，此时，我们需要知道它的文本表示，以确认当前位置是否存在等于号。用常量表示这类 Token 文本是个不错主意。\npackage lexertoken const EOF rune = 0 const LEFT_BRACKET string = \u0026#34;[\u0026#34; const RIGHT_BRACKET string = \u0026#34;]\u0026#34; const EQUAL_SIGN string = \u0026#34;=\u0026#34; const NEWLINE string = \u0026#34;\\n\u0026#34; 接下来 # 在 Part 2，我们将会对词法分析部分进行更加深入的介绍，我们在前面定义的 Token 结构也将会被用到。\n我的博文：Go 实现词法分析与解析 Part One，译：Writing a Lexer and Parser in Go - Part 1\n","date":"2019-07-17","externalUrl":null,"permalink":"/posts/2019-07-17-golang-lexer-and-parser-part1/","section":"文章","summary":"一直对词法分析与解析的话题比较感兴趣，最近发现了好几篇相关的优秀文章，准备好好翻译和研究下。我的理解，词法分析与解析的应用还是比较广泛的，无论简单的配置文件、各种模板语言、还是我们每天在写编程语言都离不开它。\n","title":"Go 实现词法分析与解析 Part One","type":"posts"},{"content":" 译者前言 # 第二篇官方博客的翻译，主要是关于 Go 并发控制的 context 包。\n总体来说，我认为上一篇才是 Go 并发的基础与核心。context 是在前章基础之上，为 goroutine 控制而开发的一套便于使用的库。毕竟，在不同的 goroutine 之间只传递 done channel，包含信息量确实是太少。\n文章简单介绍了 context 提供的方法，以及简单介绍它们如何使用。接着，通过一个搜索的例子，介绍了在真实场景下的使用。\n文章的尾部部分说明了，除了官方实现的 context，也有一些第三方的实现，比如 github.com/context 和 Tomb，但这些在官方 context 出现之后就已经停止更新了。其实原因很简单，毕竟一般都是官方更强大。之前，go 模块管理也是百花齐放，但最近官方推出自己的解决方案，或许不久，其他方式都将会淘汰。\n其实，我觉得这篇文章并不好读，感觉不够循序渐进。突然的一个例子或许会让人有点懵逼。\n正文如下：\nGo 的服务中，每个请求都会有独立的 goroutine 处理，每个 goroutine 通常会启动新的 goroutine 执行一些额外的工作，比如进行数据库或 RPC 服务的访问。同请求内的 goroutine 需能共享请求数据访问，比如，用户认证，授权 token，以及请求截止时间。如果请求取消或发生超时，请求范围内的所有 goroutine 都应立刻退出，进行资源回收.\n在 Google，我们开发了一个 context 的包，通过它，我们可以非常方便地在请求内的 goroutine 之间传递请求数据、取消信号和超时信息。详情查看 context。\n本文将会具体介绍 context 包的使用，并提供一个完整的使用案例。\nContext # context 的核心是 Context 类型。定义如下：\n// A Context carries a deadline，cancellation signal，and request-scoped values // across API. Its methods are safe for simultaneous use by multiple goroutines // 一个 Context 可以在 API (无论是否是协程间) 之间传递截止日期、取消信号、请求数据。 // Context 中的方法都是协程安全的。 type Context interface { // Done returns a channel that is closed when this context is cancelled // or times out. // Done 方法返回一个 channel，当 context 取消或超时，Done 将关闭。 Done() \u0026lt;-chan struct{} // Err indicates why this context was canceled, after the Done channel // is closed // 在 Done 关闭后，Err 可用于表明 context 被取消的原因 Err() error // Deadline returns the time when this Context will be canceled, if any. // 到期则取消 context Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none Value(key interface{}) interface{} } 介绍比较简要，详细信息查看 godoc。\nDone 方法返回的是一个 channel，它可用于接收 context 的取消信号。当 channel 关闭，监听 Done 信号的函数会立刻放弃当前正在执行的工作并返回。Err 方法返回一个 error 变量，从它之中可以知道 context 为什么被取消。pipeline and cancelation 一文对 Done channel 作了详细介绍。\n为什么 Context 没有 cancel 方法，它的原因与 Done channel 只读的原因类似，即接收取消信号的 goroutine 通过不会负责取消信号的发出。特别是，当父级启动子级 goroutine 来执行操作，子级是无法取消父级的。反之，WithCancel 方法（接下来介绍）提供了一种方式取消新创建的 Context。\nContext 是协程并发安全的。我们可以将 Context 传递给任意数量的 goroutine，通过 cancel 可以给所有的 goroutine 发送信号。\nDeadline 方法可以让函数决定是否需要启动工作，如果剩余时间太短，那么启动工作就不值得了。在代码中，我们可以通过 deadline 为 IO 操作设置超时时间。\nValue 方法可以让 context 在 goroutine 之间共享请求范围内的数据，这些数据需要是协程并发安全的。\n派生 Context # context 包提供了多个函数从已有的 Context 实例派生新的 Context。这些 Context 将会形成一个树状结构，只要一个 Context 取消，派生的 context 将都被取消。\nBackground 函数返回的 Context 是任何 Context 根，并且不可以被取消。\n// Background returns an empty Context. It is never canceled, has no deadline, // and has no values. Background is typically used in main, init, and tests， // and as the top-level Context for incoming requests. // Background 函数返回空 Context，并且不可以取消，没有最后期限，没有共享数据。Background 仅仅会被用在 main、init 或 tests 函数中。 func Background() Context WithCancel 和 WithTimeout 会派生出新的 Context 实例，派生实例比父级更早被取消。与请求关联的 Context 实例，在请求处理完成后将被取消。当遇到多副本的数据请求时，WithCancel 可用于取消多余请求。在请求后端服务时，WithTimeout 可用于设置超时时间。\n// WithCancel returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed or cancel is called. // WithCanal 返回父级 Context 副本，当父级的 Done channel 关闭或调用 cancel，它的 Done channel 也会关闭。 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // A CancelFunc cancels a Context. // CancelFunc 用于取消 Context type CancelFunc func() // WithTimeout returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed, cancel is called, or timeout elapses. The new // Context\u0026#39;s Deadline is the sooner of now+timeout and the parent\u0026#39;s deadline, if // any. If the timer is still running, the cancel function releases its // resources. // 返回父级 Context 副本和 CancelFunc，三种情况，它的 Done 会关闭，分别是父级 Done 关闭，cancel 被调用，和达到超时时间。 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) WithValue 提供了一种方式，通过 Context 传递请求相关的数据\n// WithValue returns a copy of parent whose Value method returns val for key. func WithValue(parent Context, key interface{}, val interface{}) Context context 如何使用呢？最好的方式，通过一个案例演示。\n案例：Google Web 搜索 # 演示一个案例，实现一个 HTTP 服务，处理类似 /search?q=golang\u0026amp;timeout=1s 的请求。timeout 表示如果请求处理时间超过了指定时间，取消执行。\n代码主要涉及 3 个 package，分别是：\nserver，主函数入口和 /search 处理函数； userip，实现从 request 的 context 导出 user ip 的公共函数； google，实现了 Search 函数，负责向 Google 发送搜索请求； 开始介绍!\nserver # server 负责处理类似 /search?q=golang 的请求，返回 golang 搜索结果，handleSearch 是实际的处理函数，它首先初始化了一个 Context，命名为 ctx，通过 defer 实现函数退出 cancel。如果请求参数含有 timeout，通过 WithTimeout 创建 context，在超时后，Context 将自动取消。\nfunc handleSearch(w http.ResponseWriter, req *http.Request) { // ctx is the Context for this handler. Calling cancel closes the // cxt.Done channel, which is the cancellation signal for requests // started by this handler var ( ctx context.Context cancel context.Context ) timeout, err := time.ParseDuration(req.FromValue(\u0026#34;timeout\u0026#34;)) if err != nil { // the request has a timeout, so create a context that is // canceled automatically when the timeout expires. ctx, cancel = context.WithTimeout(context.Background(), timeout) } else { ctx, cancel = context.WithCancel(context.Background(), timeout) } defer cancel() // Cancel ctx as soon as handlSearch returns. 下一步，处理函数会从请求中获取查询关键词和客户端 IP，客户端 IP 的获取通过调用 userip 包函数实现。同时，由于后端服务的请求也需要客户端 IP，故而将其附在 ctx 上。\n// Check the search query query := req.FormValue(\u0026#34;q\u0026#34;) if query == \u0026#34;\u0026#34; { http.Error(w, \u0026#34;no query\u0026#34;, http.StatusBadRequest) return } // Store the user IP in ctx for use by code in other packages. userIP, err := userip.FormRequest(req) if err != nil { http.Error(w, e.Error(), http.StatusBadRequest) return } ctx = userip.NewContext(ctx, userIP) 调用 google.Search，并传入 ctx 和 query 参数。\n// Run the Google search and print the results start := time.Now() results, err := google.Search(ctx, query) elapsed := time.Since(start) 搜索成功后，handler 渲染结果页面。\nif err := resultsTemplate.Execute(w, struct{ Results google.Results Timeout, Elapsed time.Duration }{ Results: results, Timeout: timeout, Elaplsed: elaplsed, }); err != nil { log.Print(err) return } Package userip # userip 包中提供了两个函数，负责从请求中导出用户 IP 和将用户 IP 绑定 Context 上。 Context 中包含 key-value 映射，key 与 value 的类型都是 interface{}，key 必须支持相等比较，value 要是协程并发安全的。userip 包通过对 Context 中的 value ，即 client IP 执行了类型转化，隐藏了 map 的细节。为了避免 key 的冲突，userip 定义了一个不可导出的类型 key。\n// The key type is unexported to prevent collision with context keys defined in // other package type key int // userIPkey is the context key for the user IP address. Its value of zero is // arbitrary. If this package defined other context keys, they would have // different integer values. const userIPKye key = 0 函数 FromRequest 负责从 http.Request 导出用户 IP:\nfunc FromRequest(req *http.Request) (net.IP, error) { ip, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { return nil, fmt.Errorf(\u0026#34;userip: %q is not IP:port\u0026#34;, req.RemoteAddr) } 函数 NewContext 生成一个带有 userIP 的 Context:\nfunc NewContext(ctx context.Context, userIP net.IP) context.Context { return context.WithValue(ctx, userIPKey, userIP) } FromContext 负责从 Context 中导出 userIP:\nfunc FromContext(ctx context.Context) (net.IP. bool) { // ctx.Value returns nil if ctx has no value for the key; // the net.IP type assertion returns ok=false for nil userIP, ok := ctx.Value(userIPKey).(net.IP) return userIP, ok } Package google # google.Search 负责 Google Web Search 接口的请求，以及接口返回 JSON 数据的解析。它接收 Context 类型参数 ctx，如果 ctx.Done 关闭，即使请求正在运行也将立刻返回。\n查询的请求参数包括 query 关键词和用户 IP。\nfunc Search(ctx context.Context, query string) (Results, error) { // Prepare the Google Search API request req, err := http.NewRequest(\u0026#34;GET\u0026#34;, \u0026#34;http://ajax.googleapis.com/ajax/services/search/web?v=1.0\u0026#34;, nil) if err != nil { return nil, err } q := req.URL.Query() q.Set(\u0026#34;q\u0026#34;, query) // If ctx is carrying the user IP address, forward it to the server // Google APIs use the user IP to distinguish server-initiated requests // from end-users requests if userIP, ok := userip.FromContext(ctx); ok { q.Set(\u0026#34;userip\u0026#34;, userIP.String()) } req.URL.RawQuery = q.Encode() Search 函数使用了一个帮助函数，httpDo，负责发起 HTTP 请求，如果 ctx.Done 关闭，即使请求正在执行，也会被关闭。Search 传递了一个闭包函数给 httpDo 处理响应结果。\nvar results Results err = httpDo(ctx, req, func(resp *http.Response, err error) error { if err != nil { return err } defer resp.Body.Close() // Parse the JSON search result. // https://developers.google.com/web-search/docs/#fonje var data struct { ResponseData struct { Results []struct { TitleNoFormatting string URL string } } } if err := json.NewDecoder(resp.Body).Decode(\u0026amp;data); err != nil { return err } for _, res := range data.ResponseData.Results { results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL}) } return nil }) return results, err httpDo 函数开启一个新的 goroutine 负责 HTTP 请求执行和响应结果处理。如果在 goroutine 退出前，即请求还没执行结束，如果 ctx.Done 关闭，请求执行将被取消。\nfunc httpDo(ctx context.Context, req *http.Request, f func(*http.Request, error) error) error { // Run the HTTP request in a goroutine and pass the response to f. c := make(chan error, 1) req := req.WithContext(ctx) go func() { c \u0026lt;- f(http.DefaultClient.Do(req)) }() select { case \u0026lt;-ctx.Done(): \u0026lt;- c return ctx.Err case err := \u0026lt;-c: return err } } 基于 Context 调整代码 # 许多服务端框架都提供了相应的包和数据类型进行请求的数据传递。我们可以基于 Context 接口编写新的实现代码，完成框架与处理函数的连接。\n译者注：下面介绍的就是开发说的两个 context 的第三方实现，其中有些内容需要简单了解下它们才能完全看懂。 例如，Gorilla\u0026rsquo;s 的 context 通过在请求上提供 key value 映射实现关联数据绑定。在 gorilla.go，提供了 Context 的实现，它的 Value 方法返回的值和一个具体的 HTTP 请求关联。\n其他一些包提供与 Context 类似的取消支持。例如，Tomb 中有 Kill 方法通过关闭 Dying channel 实现取消信号发出。Tomb 也提供了方法用于等待 goroutine 退出，与 sync.WaitGroup 类似。在 tomb.go 中，提供了一种实现，当父 Context 取消或 Tomb 被 kill时，当前 Context 将会取消。\n总结 # 在 Google，对于接收或发送请求类的函数，我们要求必须要将 Context 作为首个参数进行传递。如此，即使不同团队的 Go 代码也可以工作良好。Context 非常便于 goroutine 的超时与取消控制，以及确保重要数据的安全传递，比如安全凭证。\n基于 Context 的服务框架需要实现 Context，帮助连接框架和使用方，使用方期望从框架接收 Context 参数。而客户端库，则与之相反，它从调用方接收 Context 参数。context 通过为请求数据与取消控制建立通用接口，实现包开发者们可以非常轻松地共享自己的代码，以及打造出更具扩展性的服务。\n我的博文：Go 通过 Context 实现并发控制，译：blog.golang.org/context\n","date":"2019-07-12","externalUrl":null,"permalink":"/posts/2019-07-12-concurrent-using-context/","section":"文章","summary":"译者前言 # 第二篇官方博客的翻译，主要是关于 Go 并发控制的 context 包。\n总体来说，我认为上一篇才是 Go 并发的基础与核心。context 是在前章基础之上，为 goroutine 控制而开发的一套便于使用的库。毕竟，在不同的 goroutine 之间只传递 done channel，包含信息量确实是太少。\n","title":"Go 通过 Context 实现并发控制","type":"posts"},{"content":" 译者前言 # 这篇文章来自 Go 官网，不愧是官方的博客，写的非常详细。在开始翻译这篇文章前，先简单说明两点。\n首先，这篇文章我之前已经翻译过一遍，但最近再读，发现之前的翻译真是有点烂。于是，决定在完全不参考之前译文的情况下，把这篇文章重新翻译一遍。\n其二，文章中有一些专有名字，计划还是用英文来表达，以保证原汁原味，比如 pipeline（管道）、stage (阶段)、goroutine (协程)、channel (通道)。\n关于它们之间的关系，按自己的理解简单画了张草图，希望能帮助更好地理解它们之间的关系。如下：\n强调一点，如果大家在阅读这篇文章时，感到了迷糊，建议可以回头再看一下这张图。\n翻译的正文部分如下。\nGo 的并发原语使我们非常轻松地就构建出可以高效利用 IO 和多核 CPU 的流式数据 pipeline。这篇文章将会此为基础进行介绍。在这个过程中，我们将会遇到一些异常情况，关于它们的处理方法，文中也会详细介绍。\n什么是管道（pipeline） # 关于什么是管道， Go 中并没有给出明确的定义，它只是众多并发编程方式中的一种。非正式的解释，我们理解为，它是由一系列通过 chanel 连接起来的 stage 组成，而每个 stage 都是由一组运行着相同函数的 goroutine 组成。每个 stage 的 goroutine 通常会执行如下的一些工作：\n从上游的输入 channel 中接收数据； 对接收到的数据进行一些处理，（通常）并产生新的数据； 将数据通过输出 channel 发送给下游； 除了第一个 stage 和最后一个 stage ，每个 stage 都包含一定数量的输入和输出 channel。第一个 stage 只有输出，通常会把它称为 \u0026ldquo;生产者\u0026rdquo;，最后一个 stage 只有输入，通常我们会把它称为 \u0026ldquo;消费者\u0026rdquo;。\n我们先来看一个很简单例子，通过它来解释上面提到那些与 pipeline 相关的概念和技术。了解了这些后，我们再看其它的更实际的例子。\n计算平方数 # 一个涉及三个 stage 的 pipeline。\n第一个 stage，gen 函数。它负责将把从参数中拿到的一系列整数发送给指定 channel。它启动了一个 goroutine 来发送数据，当数据全部发送结束，channel 会被关闭。\nfunc gen(nums ...int) \u0026lt;-chan int { out := make(chan int) go func() { for _, n := range nums { out \u0026lt;- n } close(out) }() return out } 第二个 stage，sq 函数。它负责从输入 channel 中接收数据，并会返回一个新的 channel，即输出 channel，它负责将经过平方处理过的数据传输给下游。当输入 channel 关闭，并且所有数据都已发送到下游，就可以关闭这个输出 channel 了。\nfunc sq(in \u0026lt;-chan int) \u0026lt;-chan int { out := make(chan int) go func() { for n := range in { out \u0026lt;- n * n } close(out) }() return out } main 函数负责创建管道并执行最后一个 stage 的任务。它将从第二个 stage 接收数据，并将它们打印出来，直到 channel 关闭。\nfunc main() { // Set up the pipeline. c := gen(2, 3) out := sq(c) // Consume the output. fmt.Println(\u0026lt;-out) // 4 fmt.Println(\u0026lt;-out) // 9 } 既然，sq 的输入和输出的 channel 类型相同，那么我们就可以把它进行组合，从而形成多个 stage。比如，我们可以把 main 函数重写为如下的形式：\nfunc main() { // Set up the pipeline and consume the output. for n := range sq(sq(gen(2, 3))) { fmt.Println(n) // 16 then 81 } } 扇出和扇入（Fan-out and Fan-in） # 当多个函数从一个 channel 中读取数据，直到 channel 关闭，这称为扇出 fan-out。利用它，我们可以实现了一种分布式的工作方式，通过一组 workers 实现并行的 CPU 和 IO。\n当一个函数从多个 channel 中读取数据，直到所有 channel 关闭，这称为扇入 fan-in。扇入是通过将多个输入 channel 的数据合并到同一个输出 channel 实现的，当所有的输入 channel 关闭，输出的 channel 也将关闭。\n我们来改变一下上面例子中的管道，在它上面运行两个 sq 函数试试。它们将都从同一个输入 channel 中读取数据。我们引入了一个新的函数，merge，负责 fan-in 处理结果，即 merge 两个 sq 的处理结果。\nfunc main() { in := gen(2, 3) // Distribute the sq work across two goroutines that both read from in. // 分布式处理来自 in channel 的数据 c1 := sq(in) c2 := sq(in) // Consume the merged output from c1 and c2. // 从 channel c1 和 c2 的合并后的 channel 中接收数据 for n := range merge(c1, c2) { fmt.Println(n) // 4 then 9, or 9 then 4 } } merge 函数负责将从一系列输入 channel 中接收的数据合并到一个 channel 中。它为每个输入 channel 都启动了一个 goroutine，并将它们中接收到的值发送到惟一的输出 channel 中。在所有的 goroutines 启动后，还会再另外启动一个 goroutine，它的作用是，当所有的输入 channel 关闭后，负责关闭唯一的输出 channel 。\n在已关闭的 channel 发送数据将导致 panic，因此要保证在关闭 channel 前，所有数据都发送完成，是非常重要的。sync.WaitGroup 提供了一种非常简单的方式来完成这样的同步。\nfunc merge(cs ...\u0026lt;-chan int) \u0026lt;-chan int { var wg sync.WaitGroup out := make(chan int) // Start an output goroutine for each input channel in cs. output // copies values from c to out until c is closed, then calls wg.Done. // 为每个输入 channel 启动一个 goroutine output := func(c \u0026lt;-chan int) { for n := range c { out \u0026lt;- n } wg.Done() } wg.Add(len(cs)) for _, c := range cs { go output(c) } // Start a goroutine to close out once all the output goroutines are // done. This must start after the wg.Add call. // 启动一个 goroutine 负责在所有的输入 channel 关闭后，关闭这个唯一的输出 channel go func() { wg.Wait() close(out) }() return out } 中途停止 # 管道中的函数包含一个模式:\n当数据发送完成，每个 stage 都应该关闭它们的输入 channel； 只要输入 channel 没有关闭，每个 stage 就要持续从中接收数据； 我们可以通过编写 range loop 来保证所有 goroutine 是在所有数据都已经发送到下游的时候退出。\n但在一个真实的场景下，每个 stage 都接收完 channel 中的所有数据，是不可能的。有时，我们的设计是：接收方只需要接收数据的部分子集即可。更常见的，如果 channel 在上游的 stage 出现了错误，那么，当前 stage 就应该提早退出。无论如何，接收方都不该再继续等待接收 channel 中的剩余数据，而且，此时上游应该停止生产数据，毕竟下游已经不需要了。\n我们的例子中，即使 stage 没有成功消费完所有的数据，上游 stage 依然会尝试给下游发送数据，这将会导致程序永久阻塞。\n// Consume the first value from the output. // 从 output 中接收了第一个数据 out := merge(c1, c2) fmt.Println(\u0026lt;-out) // 4 or 9 return // Since we didn\u0026#39;t receive the second value from out, // one of the output goroutines is hung attempting to send it. // 我们并没有从 out channel 中接收第二个数据， // 所以上游的其中一个 goroutine 在尝试向下游发送数据时将会被挂起。 } 这是一种资源泄露，goroutine 是需要消耗内存和运行时资源的，goroutine 栈中的堆引用信息也是不会被 gc。\n我们需要提供一种措施，即使当下游从上游接收数据时发生异常，上游也能成功退出。一种方式是，把 channel 改为带缓冲的 channel，这样，它就可以承载指定数量的数据，如果 buffer channel 还有空间，数据的发送将会立刻完成。\n// 缓冲大小 2 buffer size 2 c := make(chan int, 2) // 发送立刻成功 succeeds immediately c \u0026lt;- 1 // 发送立刻成功 succeeds immediately c \u0026lt;- 2 //blocks until another goroutine does \u0026lt;-c and receives 1 // 阻塞，直到另一个 goroutine 从 c 中接收数据 c \u0026lt;- 3 如果我们在创建 channel 时已经知道将发送的数据量，就可以把前面的代码简化一下。比如，重写 gen 函数，将数据都发送至一个 buffer channel，这还能避免创建新的 goroutine。\nfunc gen(nums ...int) \u0026lt;-chan int { out := make(chan int, len(nums)) for _, n := range nums { out \u0026lt;- n } close(out) return out } 译者按：channel 关闭后，不可再写入数据，否则会 panic，但是仍可读取已发送数据，而且可以一直读取 0 值。\n继续往下游 stage，将又会返回到阻塞的 goroutine 中，我们也可以考虑给 merge 的输出 channel 加点缓冲。\nfunc merge(cs ...\u0026lt;-chan int) \u0026lt;-chan int { var wg sync.WaitGroup // enough space for the unread inputs // 给未读的输入 channel 预留足够的空间 out := make(chan int, 1) // ... the rest is unchanged ... 虽然通过这个方法，我们能解决了 goroutine 阻塞的问题，但是这并非一个优秀的设计。比如 merge 中的 buffer 的大小 1 是基于我们已经知道了接下来接收数据的大小，以及下游将能消费的数量。很明显，这种设计非常脆弱，如果上游多发送了一些数据，或下游并没接收那么多的数据，goroutine 将又会被阻塞。\n因而，当下游不再准备接收上游的数据时，需要有一种方式，可以通知到上游。\n明确的取消 # 如果 main 函数在没把 out 中所有数据接收完就退出，它必须要通知上游停止继续发送数据。如何做到？我们可以在上下游之间引入一个新的 channel，通常称为 done。\n示例中有两个可能阻塞的 goroutine，所以， done 需要发送两个值来通知它们。\nfunc main() { in := gen(2, 3) // Distribute the sq work across two goroutines that both read from in. c1 := sq(in) c2 := sq(in) // Consume the first value from output. done := make(chan struct{}, 2) out := merge(done, c1, c2) fmt.Println(\u0026lt;-out) // 4 or 9 // Tell the remaining senders we\u0026#39;re leaving. // 通知发送方，我们已经停止接收数据了 done \u0026lt;- struct{}{} done \u0026lt;- struct{}{} } 发送方 merge 用 select 语句替换了之前的发送操作，它负责通过 out channel 发送数据或者从 done 接收数据。done 接收的值是没有实际意义的，只是表示 out 应该停止继续发送数据了，用空 struct 即可。output 函数将会不停循环，因为上游，即 sq ，并没有阻塞。我们过会再讨论如何退出这个循环。\nfunc merge(done \u0026lt;-chan struct{}, cs ...\u0026lt;-chan int) \u0026lt;-chan int { var wg sync.WaitGroup out := make(chan int) // Start an output goroutine for each input channel in cs. output // copies values from c to out until c is closed or it receives a value // from done, then output calls wg.Done. output := func(c \u0026lt;-chan int) { for n := range c { select { case out \u0026lt;- n: case \u0026lt;-done: } } wg.Done() } // ... the rest is unchanged ... 这种方法有个问题，下游只有知道了上游可能阻塞的 goroutine 数量，才能向每个 goroutine 都发送了一个 done 信号，从而确保它们都能成功退出。但多维护一个 count 是很令人讨厌的，而且很容易出错。\n我们需要一种方式，可以告诉上游的所有 goroutine 停止向下游继续发送信息。在 Go 中，其实可通过关闭 channel 实现，因为在一个已关闭的 channel 接收数据会立刻返回，并且会得到一个零值。\n这也就意味着，main 仅需通过关闭 done channel，就可以让所有的发送方解除阻塞。关闭操作相当于一个广播信号。为确保任意返回路径下都成功调用，我们可以通过 defer 语句关闭 done。\nfunc merge(done \u0026lt;-chan struct{}, cs ...\u0026lt;-chan int) \u0026lt;-chan int { var wg sync.WaitGroup out := make(chan int) // Start an output goroutine for each input channel in cs. output // copies values from c to out until c or done is closed, then calls // wg.Done. // 为每个输入 channel 启动一个 goroutine，将输入 channel 中的数据拷贝到 // out channel 中，直到输入 channel，即 c，或 done 关闭。 // 接着，退出循环并执行 wg.Done() output := func(c \u0026lt;-chan int) { defer wg.Done() for n := range c { select { case out \u0026lt;- n: case \u0026lt;-done: return } } } // ... the rest is unchanged ... 同样地，一旦 done 关闭，sq 也将退出。sq 也是通过 defer 语句来确保自己的输出 channel，即 out，一定被成功关闭释放。\nfunc sq(done \u0026lt;-chan struct{}, in \u0026lt;-chan int) \u0026lt;-chan int { out := make(chan int) go func() { defer close(out) for n := range in { select { case out \u0026lt;- n * n: case \u0026lt;-done: return } } }() return out } 都这里，Go 中如何构建一个 pipeline，已经介绍的差不多了。\n简单总结下如何正确构建一个 pipeline。\n当所有的发送已经完成，stage 应该关闭输出 channel； stage 应该持续从输入 channel 中接收数据，除非 channel 关闭或主动通知到发送方停止发送。 Pipeline 中有量方式可以解除发送方的阻塞，一是发送方创建充足空间的 channel 来发送数据，二是当接收方停止接收数据时，明确通知发送方。\n摘要树 # 一个真实的案例。\nMD5，消息摘要算法，可用于文件校验和的计算。下面的输出是命令行工具 md5sum 输出的文件摘要信息。\n$ md5sum *.go d47c2bbc28298ca9befdfbc5d3aa4e65 bounded.go ee869afd31f83cbb2d10ee81b2b831dc parallel.go b88175e65fdcbc01ac08aaf1fd9b5e96 serial.go 我们的例子和 md5sum 类似，不同的是，传递给这个程序的参数是一个目录。程序的输出是目录下每个文件的摘要值，输出的顺序按文件名排序。\n$ go run serial.go . d47c2bbc28298ca9befdfbc5d3aa4e65 bounded.go ee869afd31f83cbb2d10ee81b2b831dc parallel.go b88175e65fdcbc01ac08aaf1fd9b5e96 serial.go 主函数，第一步调用 MD5All，它返回的是一个以文件名为 key，摘要值为 value 的 map，然后对返回结果进行排序和打印。\nfunc main() { // Calculate the MD5 sum of all files under the specified directory, // then print the results sorted by path name. m, err := MD5All(os.Args[1]) if err != nil { fmt.Println(err) return } var paths []string for path := range m { paths = append(paths, path) } sort.Strings(paths) for _, path := range paths { fmt.Printf(\u0026#34;%x %s\\n\u0026#34;, m[path], path) } } MD5All 函数将是我们接下来讨论的重点。串行版的实现没有并发，仅仅是从文件中读取数据再计算。\n// MD5All reads all the files in the file tree rooted at root and returns a map // from file path to the MD5 sum of the file\u0026#39;s contents. If the directory walk // fails or any read operation fails, MD5All returns an error. func MD5All(root string) (map[string][md5.Size]byte, error) { m := make(map[string][md5.Size]byte) err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } data, err := ioutil.ReadFile(path) if err != nil { return err } m[path] = md5.Sum(data) return nil }) if err != nil { return nil, err } return m, nil } 并行计算 # 在 并行版 中，我们会把 MD5All 的计算拆分开含有两个 stage 的 pipeline。第一个 stage，sumFiles，负责遍历目录和计算文件摘要值，摘要的计算会启动一个 goroutine 来执行，计算结果将通过一个类型 result 的 channel 发出。\ntype result struct { path string sum [md5.Size]byte err error } sumFiles 返回了 2 个 channel，一个用于接收计算的结果，一个用于接收 filepath.Walk 的 err 返回。walk 会为每个文件启动一个 goroutine 执行摘要计算和检查 done。如果 done 关闭，walk 将立刻停止。\nfunc sumFiles(done \u0026lt;-chan struct{}, root string) (\u0026lt;-chan result, \u0026lt;-chan error) { // For each regular file, start a goroutine that sums the file and sends // the result on c. Send the result of the walk on errc. c := make(chan result) errc := make(chan error, 1) go func() { var wg sync.WaitGroup err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } wg.Add(1) go func() { data, err := ioutil.ReadFile(path) select { case c \u0026lt;- result{path, md5.Sum(data), err}: case \u0026lt;-done: } wg.Done() }() // Abort the walk if done is closed. select { case \u0026lt;-done: return errors.New(\u0026#34;walk canceled\u0026#34;) default: return nil } }) // Walk has returned, so all calls to wg.Add are done. Start a // goroutine to close c once all the sends are done. go func() { wg.Wait() close(c) }() // No select needed here, since errc is buffered. // 不需要使用 select，因为 errc 是带有 buffer 的 channel errc \u0026lt;- err }() return c, errc } MD5All 将从 c channel 中接收计算的结果，如果发生错误，将通过 defer 关闭 done。\nfunc MD5All(root string) (map[string][md5.Size]byte, error) { // MD5All closes the done channel when it returns; it may do so before // receiving all the values from c and errc. done := make(chan struct{}) defer close(done) c, errc := sumFiles(done, root) m := make(map[string][md5.Size]byte) for r := range c { if r.err != nil { return nil, r.err } m[r.path] = r.sum } if err := \u0026lt;-errc; err != nil { return nil, err } return m, nil } 并行限制 # 在 并行版本 中，MD5All 为每个文件启动了一个 goroutine。但如果一个目录中文件太多，这可能会导致分配的内存过大以至于超过了当前机器的限制。\n我们可以通过限制并行读取的文件数，限制内存分配。在 并发限制版本中，我们创建了固定数量的 goroutine 读取文件。现在，我们的 pipeline 涉及 3 个 stage：遍历目录、文件读取与摘要计算、结果收集。\n第一个 stage，遍历目录并通过 paths channel 发出文件。\nfunc walkFiles(done \u0026lt;-chan struct{}, root string) (\u0026lt;-chan string, \u0026lt;-chan error) { paths := make(chan string) errc := make(chan error, 1) go func() { // Close the paths channel after Walk returns. defer close(paths) // No select needed for this send, since errc is buffered. errc \u0026lt;- filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.Mode().IsRegular() { return nil } select { case paths \u0026lt;- path: case \u0026lt;-done: return errors.New(\u0026#34;walk canceled\u0026#34;) } return nil }) }() return paths, errc } 第二个 stage，启动固定数量的 goroutine，从 paths channel 中读取文件名称，处理结果发送到 c channel。\nfunc digester(done \u0026lt;-chan struct{}, paths \u0026lt;-chan string, c chan\u0026lt;- result) { for path := range paths { data, err := ioutil.ReadFile(path) select { case c \u0026lt;- result{path, md5.Sum(data), err}: case \u0026lt;-done: return } } } 和之前的例子不同，digester 将不会关闭 c channel，因为多个 goroutine 共享这个 channel，计算结果都将发给这个 channel 上。\n相应地，MD5All 会负责在所有摘要完成后关闭这个 c channel。\n// Start a fixed number of goroutines to read and digest files. c := make(chan result) var wg sync.WaitGroup const numDigesters = 20 wg.Add(numDigesters) for i := 0; i \u0026lt; numDigesters; i++ { go func() { digester(done, paths, c) wg.Done() }() } go func() { wg.Wait() close(c) }() 我们也可以为每个 digester 创建一个单独的 channel，通过自己的 channel 传输结果。但这种方式，我们还要再启动一个新的 goroutine 合并结果。\n最后一个 stage，负责从 c 中接收处理结果，通过 errc 检查是否有错误发生。该检查无法提前进行，因为提前执行将会阻塞 walkFile 往下游发送数据。\nm := make(map[string][md5.Size]byte) for r := range c { if r.err != nil { return nil, r.err } m[r.path] = r.sum } // Check whether the Walk failed. if err := \u0026lt;-errc; err != nil { return nil, err } return m, nil } 总结 # 这篇文章介绍在 Go 中如何正确地构建流式数据 pipeline。\n它的异常处理非常复杂，pipeline 中的每个 stage 都可能导致上游阻塞，而下游可能不再关心接下来的数据。关闭 channel 可以给所有运行中的 goroutine 发送 done 信号，这能帮助我们成功解除阻塞。如何正确地构建一条流式数据 pipeline，文中也总结了一些指导建议。\n我的博文：Go 如何构建并发 Pipeline，译：Go Concurrency Patterns: Pipelines and cancellation\n","date":"2019-07-05","externalUrl":null,"permalink":"/posts/2019-07-05-golang-pipeline/","section":"文章","summary":"译者前言 # 这篇文章来自 Go 官网，不愧是官方的博客，写的非常详细。在开始翻译这篇文章前，先简单说明两点。\n","title":"Go 如何构建并发 Pipeline","type":"posts"},{"content":"如何理解 Golang 中的接口。\n个人认为，要理解 Go 的接口，一定先了解下鸭子模型。\n鸭子模型 # 那什么鸭子模型？\n鸭子模型的解释，通常会用了一个非常有趣的例子，一个东西究竟是不是鸭子，取决于它的能力。游泳起来像鸭子、叫起来也像鸭子，那么就可以是鸭子。\n动态语言，比如 Python 和 Javascript 天然支持这种特性，不过相对于静态语言，动态语言的类型缺乏了必要的类型检查。\nGo 接口设计和鸭子模型有密切关系，但又和动态语言的鸭子模型有所区别，在编译时，即可实现必要的类型检查。\n什么是 Go 接口 # Go 接口是一组方法的集合，可以理解为抽象的类型。它提供了一种非侵入式的接口。任何类型，只要实现了该接口中方法集，那么就属于这个类型。\n举个例子，假设定义一个鸭子的接口。如下：\ntype Duck interface { Quack() // 鸭子叫 DuckGo() // 鸭子走 } 假设现在有一个鸡类型，结构如下：\ntype Chicken struct { } func (c Chicken) IsChicken() bool { fmt.Println(\u0026#34;我是小鸡\u0026#34;) } 这只鸡和一般的小鸡不一样，它比较聪明，也可以做鸭子能做的事情。\nfunc (c Chicken) Quack() { fmt.Println(\u0026#34;嘎嘎\u0026#34;) } func (c Chicken) DuckGo() { fmt.Println(\u0026#34;大摇大摆的走\u0026#34;) } 注意，这里只是实现了 Duck 接口方法，并没有将鸡类型和鸭子接口显式绑定。这是一种非侵入式的设计。\n我们定义一个函数，负责执行鸭子能做的事情。\nfunc DoDuck(d Duck) { d.Quack() d.DuckGo() } 因为小鸡实现了鸭子的所有方法，所以小鸡也是鸭。那么在 main 函数中就可以这么写了。\nfunc main() { c := Chicken{} DoDuck(c) } 执行正常。如此是不是很类似于其他语言的多态，其实这就是 Go 多态的实现方法。\n空接口 # 继续说说空 interface。\n如果一个 interface 中如果没有定义任何方法，即为空 interface，表示为 interface{}。如此一来，任何类型就都能满足它，这也是为什么当函数参数类型为 interface{} 时，可以给它传任意类型的参数。\n示例代码，如下：\npackage main import \u0026#34;fmt\u0026#34; func main() { var i interface{} = 2 fmt.Println(i) } 更常用的场景，Go 的 interface{} 常常会被作为函数的参数传递，用以帮助我们实现其他语言中的泛型效果。Go 中暂时不支持 泛型，不过 Go 2 的方案中似乎将支持泛型。\n总结 # 回答结束，做个简单总结。理解 Go 接口要记住一点，接口是一组方法的集合，这句话非常重要，理解了这句话，再去理解 Go 的其他知识，比如类型、多态、空接口、反射、类型检查与断言等就会容易很多。\n我的博文：如何理解 Go 的接口\n","date":"2019-06-18","externalUrl":null,"permalink":"/posts/2019-06-18-understand-golang-interface/","section":"文章","summary":"如何理解 Golang 中的接口。\n个人认为，要理解 Go 的接口，一定先了解下鸭子模型。\n鸭子模型 # 那什么鸭子模型？\n","title":"如何理解 Go 的接口","type":"posts"},{"content":"上篇文章说到，防止 goroutine 泄露可从两个角度出发，分别是代码层面的预防与运行层面的监控检测。今天，我们来谈第二点。\n简述 # 前文已经介绍了一种简单检测 goroutine 是否泄露的方法，即通过 runtime.NumGoroutine 获取当前运行中的 goroutine 数量粗略估计。但 NumGoroutine 是否真的能确定我们代码存在泄露，除此之外，还有没有其他更优的方式吗。\n注：为了更好的演示效果，下面将会用常驻的 http 作为示例。\nNumGoroutine # runtime.NumGoroutine 可以获取当前进程中正在运行的 goroutine 数量，观察这个数字可以初步判断出是否存在 goroutine 泄露异常。\n一个示例，如下：\npackage main import ( \u0026#34;net/http\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;strconv\u0026#34; ) func write(w http.ResponseWriter, data []byte) { _, _ = w.Write(data) } func count(w http.ResponseWriter, r *http.Request) { write([]byte(strconv.Itoa(runtime.NumGoroutine()))) } func main() { http.HandleFunc(\u0026#34;/_count\u0026#34;, count) http.ListenAndServe(\u0026#34;:6080\u0026#34;, nil) } 功能很简单，设置 _count 路由请求处理函数 count，它负责输出服务当前 goroutine 数量。启动服务后访问 localhost:6080/_count 即可。\n但只是一个数值，我们就能确认是否泄露了吗？\n首先，如果这个数值很大，是不是就能说明出现了泄露。我的答案是否。理由很简单，高并发情况下的 goroutine 数量肯定很高的，但并非出现了泄露，可能只是当前的服务的承载能力还不够。我们可以在数量基础上引入时间，即如果 goroutine 随着时间增加，数量在不断上升，而基本没有下降，基本可以确定存在泄露。我们可以定时采集不同时刻的数据来分析。\n演示案例 # 为了更好的演示效果，我们为服务再增加一个处理函数 query， 并绑定路由 /query 上。假设它负责从多个数据表中查出数据返回给用户。这个例子在后面的演示会一直使用。\n代码如下：\nfunc query(w http.ResponseWriter, r *http.Request) { c := make(chan byte) go func() { c \u0026lt;- 0x31 }() go func() { c \u0026lt;- 0x32 }() go func() { c \u0026lt;- 0x33 }() rs := make([]byte, 0) for i := 0; i \u0026lt; 2; i++ { rs = append(rs, \u0026lt;-c) } write(w, rs) } 在 query 中，我们启动了 3 个 goroutine 执行数据库查询，通过 channel 传递返回数据。这里的问题是，query 函数中只从 channel 中接收两次数据就退出了循环，这会导致其中一个 goroutine 因缺少接收者而无法释放。\n我们可以多次请求 localhost:6080/query，然后通过 _count 查看服务当前的 goroutine 数量。手动麻烦，可以用 ab 命令进行做个简单压测。\n$ ab -n 1000 -c 100 localhost:6080/query 命令的意思是，总共访问 1000 次，并发访问 100 次。\npprof # 前面的例子比较简单，发现泄露后，我们可以立刻确定存在的问题。但如果比较复杂的项目，我们就很难发现问题代码的出现位置了。\n如何解决呢？\n我们可以引入一个辅助工具，pprof。它是由 Go 官方提供的可用于收集程序运行时报告的工具，其中包含 CPU、内存等信息。当然，也可以获取运行时 goroutine 堆栈信息，如此一来，我们就可以很容易看出哪里导致了 goroutine 泄露。\nruntime/pprof # 我们可以再加入一个名为 goroutineStack 的 handler，用于查看程序中 goroutine 的堆栈信息，，地址为 _goroutine。\n实现代码如下：\nimport \u0026#34;runtime/pprof\u0026#34; func goroutineStack(w http.ResponseWriter, r *http.Request) { _ = pprof.Lookup(\u0026#34;goroutine\u0026#34;).WriteTo(w, 1) } 访问 _goroutine，将会得到类似如下的信息：\ngoroutine profile: total 1004 948 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b37 0x10595d1 #\t0x1233b36\tmain.query.func2+0x36\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:20 45 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233ae7 0x10595d1 #\t0x1233ae6\tmain.query.func1+0x36\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:16 7 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b87 0x10595d1 #\t0x1233b86\tmain.query.func3+0x36\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:24 1 @ 0x102e70b 0x1029ba9 0x1029256 0x108b7da 0x108b8ed 0x108c216 0x112f80f 0x113b348 0x11f5f6a 0x10595d1 #\t0x1029255\tinternal/poll.runtime_pollWait+0x65\t/usr/local/go/src/runtime/netpoll.go:173 #\t0x108b7d9\tinternal/poll.(*pollDesc).wait+0x99\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:85 #\t0x108b8ec\tinternal/poll.(*pollDesc).waitRead+0x3c\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:90 #\t0x108c215\tinternal/poll.(*FD).Read+0x1d5\t/usr/local/go/src/internal/poll/fd_unix.go:169 #\t0x112f80e\tnet.(*netFD).Read+0x4e\t/usr/local/go/src/net/fd_unix.go:202 #\t0x113b347\tnet.(*conn).Read+0x67\t/usr/local/go/src/net/net.go:177 #\t0x11f5f69\tnet/http.(*connReader).backgroundRead+0x59\t/usr/local/go/src/net/http/server.go:676 1 @ 0x102e70b 0x1029ba9 0x1029256 0x108b7da 0x108b8ed 0x108c216 0x112f80f 0x113b348 0x11f63ec 0x10fb596 0x10fbf76 0x10fc174 0x119ebbf 0x119eaeb 0x11f315c 0x11f7672 0x11fb23e 0x10595d1 #\t0x1029255\tinternal/poll.runtime_pollWait+0x65\t/usr/local/go/src/runtime/netpoll.go:173 #\t0x108b7d9\tinternal/poll.(*pollDesc).wait+0x99\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:85 #\t0x108b8ec\tinternal/poll.(*pollDesc).waitRead+0x3c\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:90 #\t0x108c215\tinternal/poll.(*FD).Read+0x1d5\t/usr/local/go/src/internal/poll/fd_unix.go:169 #\t0x112f80e\tnet.(*netFD).Read+0x4e\t/usr/local/go/src/net/fd_unix.go:202 #\t0x113b347\tnet.(*conn).Read+0x67\t/usr/local/go/src/net/net.go:177 #\t0x11f63eb\tnet/http.(*connReader).Read+0xfb\t/usr/local/go/src/net/http/server.go:786 #\t0x10fb595\tbufio.(*Reader).fill+0x105\t/usr/local/go/src/bufio/bufio.go:100 #\t0x10fbf75\tbufio.(*Reader).ReadSlice+0x35\t/usr/local/go/src/bufio/bufio.go:341 #\t0x10fc173\tbufio.(*Reader).ReadLine+0x33\t/usr/local/go/src/bufio/bufio.go:370 #\t0x119ebbe\tnet/textproto.(*Reader).readLineSlice+0x6e\t/usr/local/go/src/net/textproto/reader.go:55 #\t0x119eaea\tnet/textproto.(*Reader).ReadLine+0x2a\t/usr/local/go/src/net/textproto/reader.go:36 #\t0x11f315b\tnet/http.readRequest+0x8b\t/usr/local/go/src/net/http/request.go:958 #\t0x11f7671\tnet/http.(*conn).readRequest+0x161\t/usr/local/go/src/net/http/server.go:966 #\t0x11fb23d\tnet/http.(*conn).serve+0x49d\t/usr/local/go/src/net/http/server.go:1788 1 @ 0x102e70b 0x1029ba9 0x1029256 0x108b7da 0x108b8ed 0x108ce80 0x112fd92 0x1142c5e 0x1141967 0x11ff7df 0x121da4c 0x11fed5f 0x11fea16 0x11ff534 0x1233a91 0x102e317 0x10595d1 #\t0x1029255\tinternal/poll.runtime_pollWait+0x65\t/usr/local/go/src/runtime/netpoll.go:173 #\t0x108b7d9\tinternal/poll.(*pollDesc).wait+0x99\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:85 #\t0x108b8ec\tinternal/poll.(*pollDesc).waitRead+0x3c\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:90 #\t0x108ce7f\tinternal/poll.(*FD).Accept+0x19f\t/usr/local/go/src/internal/poll/fd_unix.go:384 #\t0x112fd91\tnet.(*netFD).accept+0x41\t/usr/local/go/src/net/fd_unix.go:238 #\t0x1142c5d\tnet.(*TCPListener).accept+0x2d\t/usr/local/go/src/net/tcpsock_posix.go:139 #\t0x1141966\tnet.(*TCPListener).AcceptTCP+0x46\t/usr/local/go/src/net/tcpsock.go:247 #\t0x11ff7de\tnet/http.tcpKeepAliveListener.Accept+0x2e\t/usr/local/go/src/net/http/server.go:3232 #\t0x11fed5e\tnet/http.(*Server).Serve+0x22e\t/usr/local/go/src/net/http/server.go:2826 #\t0x11fea15\tnet/http.(*Server).ListenAndServe+0xb5\t/usr/local/go/src/net/http/server.go:2764 #\t0x11ff533\tnet/http.ListenAndServe+0x73\t/usr/local/go/src/net/http/server.go:3004 #\t0x1233a90\tmain.main+0xb0\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:40 #\t0x102e316\truntime.main+0x206\t/usr/local/go/src/runtime/proc.go:201 1 @ 0x122ce28 0x122cc30 0x1229694 0x1233723 0x11fc194 0x11fde37 0x11fe8eb 0x11fb3e6 0x10595d1 #\t0x122ce27\truntime/pprof.writeRuntimeProfile+0x97\t/usr/local/go/src/runtime/pprof/pprof.go:707 #\t0x122cc2f\truntime/pprof.writeGoroutine+0x9f\t/usr/local/go/src/runtime/pprof/pprof.go:669 #\t0x1229693\truntime/pprof.(*Profile).WriteTo+0x3e3\t/usr/local/go/src/runtime/pprof/pprof.go:328 #\t0x1233722\tstudy/goroutine/leak/06/leak.GoroutineStack+0x92\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/leak/handlers.go:19 #\t0x11fc193\tnet/http.HandlerFunc.ServeHTTP+0x43\t/usr/local/go/src/net/http/server.go:1964 #\t0x11fde36\tnet/http.(*ServeMux).ServeHTTP+0x126\t/usr/local/go/src/net/http/server.go:2361 #\t0x11fe8ea\tnet/http.serverHandler.ServeHTTP+0xaa\t/usr/local/go/src/net/http/server.go:2741 #\t0x11fb3e5\tnet/http.(*conn).serve+0x645\t/usr/local/go/src/net/http/server.go:1847 首先是第一行，如下：\ngoroutine profile: total 1004 统计信息，和 NumGoroutine 的返回结果相同。当前共有 1004 个 goroutine 在运行。\n接下来的部分，主要是具体介绍每个 goroutine 的情况，相同函数的 goroutine 会被合并统计，并按数量从大到小排序。输出前三段就是我们在 query 函数中开启的三个 goroutine。\n948 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b37 0x10595d1 #\t0x1233b36\tmain.query.func2+0x36\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:20 45 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233ae7 0x10595d1 #\t0x1233ae6\tmain.query.func1+0x36\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:16 7 @ 0x102e70b 0x102e7b3 0x10068ed 0x10066c5 0x1233b87 0x10595d1 #\t0x1233b86\tmain.query.func3+0x36\t/Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:24 分别是 main.query.func1、main.query.func2 以及 main.query.func3，对应于它们，当前仍在运行中的 goroutine 数量分别是 45、948、7。看样子泄露的 goroutine 函数分布并非均匀。\n几个函数都是匿名的，如果我们需要确定具体位置，可以通过堆栈实现。比如 func1，明确指出了位于的所在文件和代码行数。\nhttp/net/pprof # 前面部分是通过自己编写代码把 goroutine 的分析统计指标加入到了 HTTP 服务中。其实，官方已经实现了这个功能，并且涉及的不仅仅是 goroutine，还有 CPU、内存等。\n它的操作很简单，我们只需要在服务启动时导入 net/http/pprof 即可。接着访问地址 /debug/pprof/goroutine?debug=1，将会可以看到与上一节输出的相同内容。\ngops # 熟悉 Java 的朋友都知道 jps 这个命令。通过它，我们可以查看当前机器上有哪些 Java 程序在运行。Go 也有类似的命令，gops，它支持列出当前环境下的 Go 进程，并支持对 Go 程序的诊断。默认情况下，gops 可列出并不支持对进程进行成诊断。\n今天，我们将只看它和 goroutine 相关的部分。\n一个示例，如下：\n$ gops 97778 96800 gops go1.11.1 /usr/local/go/bin/gops 97605 73594 leaker* go1.11.1 /Users/polo/Public/Work/go/src/study/goroutine/leak/06/leaker 我的环境下当前只有两个 go 进程在运行。\n仔细观察后，我们会发现 leaker 进程相比 gops 后面多个 * 的标号，而 * 表示这个程序支持通过 gops 诊断。这是因为我们在 leaker 加入了诊断支持的代码，如下：\nfunc main() { if err := agent.Listen(agent.Options{ShutdownCleanup: true}); err != nil { log.Fatalln(err) } ... } 执行如下命令，查看当前的 goroutine 数量。\n$ gops stats 97605 goroutines: 1004 OS threads: 14 GOMAXPROCS: 8 num CPU: 8 其中，97605 是进程 PID。\n结果显示，当前在运行的 goroutine 有 1004 个。而且，我们还注意到 OS 级别的线程才 14 个，可见 goroutine 的轻量。\ngops 也可以查看堆栈，我们只需执行 gops stack PID 即可，这个就不具体演示了。要说明的是，这种方式并不会对运行相同函数的 goroutine 做聚合统计，不知道是我没找到还是本身不支持。如果的确不支持，也可以自己聚合，但毕竟没那么方便。\nLeak Test # 除了出现问题后的检测调试，但如果我们能把泄露检测过程加入到自动化测试中，在正式上线前就避免，岂不是更完美。我们可以通过一个开源包实现，包的名称是 leaktest，即泄露测试的意思。\n利用 leaktest，我们测试下前面写的 http 处理函数 query。因为要检测 handler 是否泄露，如果经过网络就会丢失服务端的相关信息，这时，我们可以借助 Go 中的 net/http/test 包完成测试。\n代码如下：\nfunc Test_Query(t *testing.T) { defer leaktest.Check(t)() //创建一个请求 req, err := http.NewRequest(\u0026#34;GET\u0026#34;, \u0026#34;/query\u0026#34;, nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() //直接使用 query(rr,req) query(rr, req) // 其他测试 // ... } 测试执行输出如下：\n=== RUN Test_Query --- FAIL: Test_Query (5.01s) leaktest.go:162: leaktest: context canceled leaktest.go:168: leaktest: leaked goroutine: goroutine 20 [chan send]: study/goroutine/leak/06.query.func2(0xc0001481e0) /Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:24 +0x37 created by study/goroutine/leak/06.query /Users/polo/Public/Work/go/src/study/goroutine/leak/06/main.go:23 +0x7e FAIL 从输出信息中，我们可以明确地知道出现了泄露，并且通过输出堆栈很快就能定位出现问题的代码。测试代码非常简单，在测试函数开始通过 defer 执行 leaktest 的 Check。\n它提供的三个检测函数，分别是 Check、CheckTimeout 和 CheckContext，从前到后的实现一个比一个底层。Check 默认会等待五秒再执行检测，如果需要改变这个时间，可以使用 CheckTimeout 函数。\nleaktest 的实现原理也和堆栈有关，源码不多，如果有兴趣可以读读，源码文件地址。\n总结 # 本系列文章分别从代码实现和监控检测两个角度介绍了如何避免 goroutine 的泄露。Go 的并发降低了并发程序的开发难度，但并发一直都是个比较复杂的话题，为了用好它，必要学习还是不可缺少的。\n博文地址：如何防止你的 Goroutine 泄露\n参考资料 # Goroutine leak\ngops 工作原理\ngops - Go 语言程序查看和诊断工具\ngops — Go 程序诊断分析工具\n性能调式：分析并优化 Go 程序\nDebugging Go Routine leaks\n视频-Debugging Go routine leaks\nHTTP 测试辅助工具\n","date":"2019-06-17","externalUrl":null,"permalink":"/posts/2019-06-17-prevent-goroutine-from-leaking-part-2/","section":"文章","summary":"上篇文章说到，防止 goroutine 泄露可从两个角度出发，分别是代码层面的预防与运行层面的监控检测。今天，我们来谈第二点。\n简述 # 前文已经介绍了一种简单检测 goroutine 是否泄露的方法，即通过 runtime.NumGoroutine 获取当前运行中的 goroutine 数量粗略估计。但 NumGoroutine 是否真的能确定我们代码存在泄露，除此之外，还有没有其他更优的方式吗。\n","title":"如何防止你的 Goroutine 泄露(二)","type":"posts"},{"content":"今天简单谈谈，Go 如何防止 goroutine 泄露。\n概述 # Go 的并发模型与其他语言不同，虽说它简化了并发程序的开发难度，但如果不了解使用方法，常常会遇到 goroutine 泄露的问题。虽然 goroutine 是轻量级的线程，占用资源很少，但如果一直得不到释放并且还在不断创建新协程，毫无疑问是有问题的，并且是要在程序运行几天，甚至更长的时间才能发现的问题。\n对于上面描述的问题，我觉得可以从两方面入手解决，如下：\n一是预防，要做到预防，我们就需要了解什么样的代码会产生泄露，以及了解正确的写法是如何的；\n二是监控，虽说预防减少了泄露产生的概率，但没有人敢说自己不犯错，因而，通常我们还需要一些监控手段进一步保证程序的健壮性；\n接下来，我将会分两篇文章分别从这两个角度进行介绍，今天先谈第一点。\n如何监控泄露 # 本文主要集中在第一点上，但为了更好的演示效果，可以先介绍一个最简单的监控方式。通过 runtime.NumGoroutine() 获取当前运行中的 goroutine 数量，通过它确认是否发生泄漏。它的使用非常简单，就不为它专门写个例子了。\n一个简单的例子 # 语言级别的并发支持是 Go 的一大优势，但这个优势也很容易被滥用。通常我们在开始 Go 并发学习时，常常听别人说，Go 的并发非常简单，在调用函数前加上 go 关键词便可启动 goroutine，即一个并发单元，但很多人可能只听到了这句话，然后就出现了类似下面的代码：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; ) func sayHello() { for { fmt.Println(\u0026#34;Hello gorotine\u0026#34;) time.Sleep(time.Second) } } func main() { defer func() { fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() go sayHello() fmt.Println(\u0026#34;Hello main\u0026#34;) } 对 Go 比较熟悉的话，很容易发现这段代码的问题，sayHello 是个死循环，没有如何退出机制，因此也就没有任何办法释放创建的 goroutine。我们通过在 main 函数最前面的 defer 实现在函数退出时打印当前运行中的 goroutine 数量，毫无意外，它的输出如下：\nthe number of goroutines: 2 不过，因为上面的程序并非常驻，有泄露问题也不大，程序退出后系统会自动回收运行时资源。但如果这段代码在常驻服务中执行，比如 http server，每接收到一个请求，便会启动一次 sayHello，时间流逝，每次启动的 goroutine 都得不到释放，你的服务将会离奔溃越来越近。\n这个例子比较简单，我相信，对 Go 的并发稍微有点了解的朋友都不会犯这个错。\n泄露情况分类 # 前面介绍的例子由于在 goroutine 运行死循环导致的泄露。接下来，我会按照并发的数据同步方式对泄露的各种情况进行分析。简单可归于两类，即：\nchannel 导致的泄露 传统同步机制导致的泄露 传统同步机制主要指面向共享内存的同步机制，比如排它锁、共享锁等。这两种情况导致的泄露还是比较常见的。go 由于 defer 的存在，第二类情况，一般情况下还是比较容易避免的。\nchanel 引起的泄露 # 先说 channel，如果之前读过官方的那篇并发的文章，翻译版，你会发现 channel 的使用，一个不小心就泄露了。我们来具体总结下那些情况下可能导致。\n发送不接收 # 我们知道，发送者一般都会配有相应的接收者。理想情况下，我们希望接收者总能接收完所有发送的数据，这样就不会有任何问题。但现实是，一旦接收者发生异常退出，停止继续接收上游数据，发送者就会被阻塞。这个情况在 前面说的文章 中有非常细致的介绍。\n示例代码：\npackage main import \u0026#34;time\u0026#34; func gen(nums ...int) \u0026lt;-chan int { out := make(chan int) go func() { for _, n := range nums { out \u0026lt;- n } close(out) }() return out } func main() { defer func() { fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() // Set up the pipeline. out := gen(2, 3) for n := range out { fmt.Println(n) // 2 time.Sleep(5 * time.Second) // done thing, 可能异常中断接收 if true { // if err != nil break } } } 例子中，发送者通过 out chan 向下游发送数据，main 函数接收数据，接收者通常会依据接收到的数据做一些具体的处理，这里用 Sleep 代替。如果这期间发生异常，导致处理中断，退出循环。gen 函数中启动的 goroutine 并不会退出。\n如何解决？\n此处的主要问题在于，当接收者停止工作，发送者并不知道，还在傻傻地向下游发送数据。故而，我们需要一种机制去通知发送者。我直接说答案吧，就不循渐进了。Go 可以通过 channel 的关闭向所有的接收者发送广播信息。\n修改后的代码：\npackage main import \u0026#34;time\u0026#34; func gen(done chan struct{}, nums ...int) \u0026lt;-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { select { case out \u0026lt;- n: case \u0026lt;-done: return } } }() return out } func main() { defer func() { time.Sleep(time.Second) fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() // Set up the pipeline. done := make(chan struct{}) defer close(done) out := gen(done, 2, 3) for n := range out { fmt.Println(n) // 2 time.Sleep(5 * time.Second) // done thing, 可能异常中断接收 if true { // if err != nil break } } } 函数 gen 中通过 select 实现 2 个 channel 的同时处理。当异常发生时，将进入 \u0026lt;-done 分支，实现 goroutine 退出。这里为了演示效果，保证资源顺利释放，退出时等待了几秒保证释放完成。\n执行后的输出如下：\nthe number of goroutines: 1 现在只有主 goroutine 存在。\n接收不发送 # 发送不接收会导致发送者阻塞，反之，接收不发送也会导致接收者阻塞。直接看示例代码，如下：\npackage main func main() { defer func() { time.Sleep(time.Second) fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() var ch chan struct{} go func() { ch \u0026lt;- struct{}{} }() } 运行结果显示：\nthe number of goroutines: 2 当然，我们正常不会遇到这么傻的情况发生，现实工作中的案例更多可能是发送已完成，但是发送者并没有关闭 channel，接收者自然也无法知道发送完毕，阻塞因此就发生了。\n解决方案是什么？那当然就是，发送完成后一定要记得关闭 channel。\nnil channel # 向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。\n示例代码：\nfunc main() { defer func() { time.Sleep(time.Second) fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() var ch chan int go func() { \u0026lt;-ch // ch\u0026lt;- }() } 两种写法：\u0026lt;-ch 和 ch\u0026lt;- 1，分别表示接收与发送，都将会导致阻塞。如果想实现阻塞，通过 nil channel 和 done channel 结合实现阻止 main 函数的退出，这或许是可以一试的方法。\nfunc main() { defer func() { time.Sleep(time.Second) fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() done := make(chan struct{}) var ch chan int go func() { defer close(done) }() select { case \u0026lt;-ch: case \u0026lt;-done: return } } 在 goroutine 执行完成，检测到 done 关闭，main 函数退出。\n真实的场景 # 真实的场景肯定不会像案例中的简单，可能涉及多阶段 goroutine 之间的协作，某个 goroutine 可能即使接收者又是发送者。但归根接底，无论什么使用模式。都是把基础知识组织在一起的合理运用。\n传统同步机制 # 虽然，一般推荐 Go 并发数据的传递，但有些场景下，显然还是使用传统同步机制更合适。Go 中提供传统同步机制主要在 sync 和 atomic 两个包。接下来，我主要介绍的是锁和 WaitGroup 可能导致 goroutine 的泄露。\nMutex # 和其他语言类似，Go 中存在两种锁，排它锁和共享锁，关于它们的使用就不作介绍了。我们以排它锁为例进行分析。\n示例如下：\nfunc main() { total := 0 defer func() { time.Sleep(time.Second) fmt.Println(\u0026#34;total: \u0026#34;, total) fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() var mutex sync.Mutex for i := 0; i \u0026lt; 2; i++ { go func() { mutex.Lock() total += 1 }() } } 执行结果如下：\ntotal: 1 the number of goroutines: 2 这段代码通过启动两个 goroutine 对 total 进行加法操作，为防止出现数据竞争，对计算部分做了加锁保护，但并没有及时的解锁，导致 i = 1 的 goroutine 一直阻塞等待 i = 0 的 goroutine 释放锁。可以看到，退出时有 2 个 goroutine 存在，出现了泄露，total 的值为 1。\n怎么解决？因为 Go 有 defer 的存在，这个问题还是非常容易解决的，只要记得在 Lock 的时候，记住 defer Unlock 即可。\n示例如下：\nmutex.Lock() defer mutext.Unlock() 其他的锁与这里其实都是类似的。\nWaitGroup # WaitGroup 和锁有所差别，它类似 Linux 中的信号量，可以实现一组 goroutine 操作的等待。使用的时候，如果设置了错误的任务数，也可能会导致阻塞，导致泄露发生。\n一个例子，我们在开发一个后端接口时需要访问多个数据表，由于数据间没有依赖关系，我们可以并发访问，示例如下：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) func handle() { var wg sync.WaitGroup wg.Add(4) go func() { fmt.Println(\u0026#34;访问表1\u0026#34;) wg.Done() }() go func() { fmt.Println(\u0026#34;访问表2\u0026#34;) wg.Done() }() go func() { fmt.Println(\u0026#34;访问表3\u0026#34;) wg.Done() }() wg.Wait() } func main() { defer func() { time.Sleep(time.Second) fmt.Println(\u0026#34;the number of goroutines: \u0026#34;, runtime.NumGoroutine()) }() go handle() time.Sleep(time.Second) } 执行结果如下：\nthe number of goroutines: 2 出现了泄露。再看代码，它的开始部分定义了类型为 sync.WaitGroup 的变量 wg，设置并发任务数为 4，但是从例子中可以看出只有 3 个并发任务。故最后的 wg.Wait() 等待退出条件将永远无法满足，handle 将会一直阻塞。\n怎么防止这类情况发生？\n我个人的建议是，尽量不要一次设置全部任务数，即使数量非常明确的情况。因为在开始多个并发任务之间或许也可能出现被阻断的情况发生。最好是尽量在任务启动时通过 wg.Add(1) 的方式增加。\n示例如下：\n... wg.Add(1) go func() { fmt.Println(\u0026#34;访问表1\u0026#34;) wg.Done() }() wg.Add(1) go func() { fmt.Println(\u0026#34;访问表2\u0026#34;) wg.Done() }() wg.Add(1) go func() { fmt.Println(\u0026#34;访问表3\u0026#34;) wg.Done() }() ... 总结 # 大概介绍完了我认为的所有可能导致 goroutine 泄露的情况。总结下来，其实无论是死循环、channel 阻塞、锁等待，只要是会造成阻塞的写法都可能产生泄露。因而，如何防止 goroutine 泄露就变成了如何防止发生阻塞。为进一步防止泄露，有些实现中会加入超时处理，主动释放处理时间太长的 goroutine。\n本篇主要从如何写出正确代码的角度来介绍如何防止 goroutine 的泄露。下篇[https://juejin.im/post/5d3d76066fb9a07ee463aba0]，将会介绍如何实现更好的监控检测，以帮助我们发现当前代码中已经存在的泄露。\n博文地址：如何防止你的 Goroutine 泄露\n","date":"2019-06-10","externalUrl":null,"permalink":"/posts/2019-06-10-prevent-goroutine-from-leaking/","section":"文章","summary":"今天简单谈谈，Go 如何防止 goroutine 泄露。\n概述 # Go 的并发模型与其他语言不同，虽说它简化了并发程序的开发难度，但如果不了解使用方法，常常会遇到 goroutine 泄露的问题。虽然 goroutine 是轻量级的线程，占用资源很少，但如果一直得不到释放并且还在不断创建新协程，毫无疑问是有问题的，并且是要在程序运行几天，甚至更长的时间才能发现的问题。\n","title":"如何防止你的 Goroutine 泄露","type":"posts"},{"content":"在Go编程中，数据结构的选择对解决问题至关重要。本文将探讨如何使用set和bitset两种数据结构，以及它们在Go中的应用场景。\nGo 的数据结构 # Go 内置的数据结构并不多。工作中，我们最常用的两种数据结构分别是 slice 和 map，即切片和映射。 其实，Go 中也有数组，切片的底层就是数组，只不过因为切片的存在，我们平时很少使用它。\n除了 Go 内置的数据结构，还有一些数据结构是由 Go 的官方 container 包提供，如 heap 堆、list 双向链表和ring 回环链表。但今天我们不讲它们，这些数据结构，对于熟手来说，看看文档就会使用了。\n我们今天将来聊的是 set 和 bitset。据我所知，其他一些语言，比如 Java，是有这两种数据结构。但 Go 当前还没有以任何形式提供。\n实现思路 # 先来看一篇文章，访问地址 2 basic set implementations 阅读。文中介绍了两种 go 实现 set 的思路， 分别是 map 和 bitset。\n有兴趣可以读读这篇文章，我们接下来具体介绍下。\nmap # 我们知道，map 的 key 肯定是唯一的，而这恰好与 set 的特性一致，天然保证 set 中成员的唯一性。而且通过 map 实现 set，在检查是否存在某个元素时可直接使用 _, ok := m[key] 的语法，效率高。\n先来看一个简单的实现，如下：\nset := make(map[string]bool) // New empty set set[\u0026#34;Foo\u0026#34;] = true // Add for k := range set { // Loop fmt.Println(k) } delete(set, \u0026#34;Foo\u0026#34;) // Delete size := len(set) // Size exists := set[\u0026#34;Foo\u0026#34;] // Membership 通过创建 map[string]bool 来存储 string 的集合，比较容易理解。但这里还有个问题，map 的 value 是布尔类型，这会导致 set 多占一定内存空间，而 set 不该有这个问题。\n怎么解决这个问题？\n设置 value 为空结构体，在 Go 中，空结构体不占任何内存。当然，如果不确定，也可以来证明下这个结论。\nunsafe.Sizeof(struct{}{}) // 结果为 0 优化后的代码，如下：\ntype void struct{} var member void set := make(map[string]void) // New empty set set[\u0026#34;Foo\u0026#34;] = member // Add for k := range set { // Loop fmt.Println(k) } delete(set, \u0026#34;Foo\u0026#34;) // Delete size := len(set) // Size _, exists := set[\u0026#34;Foo\u0026#34;] // Membership 之前在网上看到有人按这个思路做了封装，还写了一篇文章，可以去读一下。\n其实，github 上已经有个成熟的包，名为 golang-set，它也是采用这个思路实现的。访问地址 golang-set，描述中说 Docker 用的也是它。包中提供了两种 set 实现，线程安全的 set 和非线程安全的 set。\n演示一个简单的案例。\npackage main import ( \u0026#34;fmt\u0026#34; mapset \u0026#34;github.com/deckarep/golang-set\u0026#34; ) func main() { // 默认创建的线程安全的，如果无需线程安全 // 可以使用 NewThreadUnsafeSet 创建，使用方法都是一样的。 s1 := mapset.NewSet(1, 2, 3, 4) fmt.Println(\u0026#34;s1 contains 3: \u0026#34;, s1.Contains(3)) fmt.Println(\u0026#34;s1 contains 5: \u0026#34;, s1.Contains(5)) // interface 参数，可以传递任意类型 s1.Add(\u0026#34;poloxue\u0026#34;) fmt.Println(\u0026#34;s1 contains poloxue: \u0026#34;, s1.Contains(\u0026#34;poloxue\u0026#34;)) s1.Remove(3) fmt.Println(\u0026#34;s1 contains 3: \u0026#34;, s1.Contains(3)) s2 := mapset.NewSet(1, 3, 4, 5) // 并集 fmt.Println(s1.Union(s2)) } 输出如下：\ns1 contains 3: true s1 contains 5: false s1 contains poloxue: true s1 contains 3: false Set{4, polxue, 1, 2, 3, 5} 例子中演示了简单的使用方式，如果有不明白的，看下源码，这些数据结构的操作方法名都是很常见的，比如交集 Intersect、差集 Difference 等，一看就懂。\nbitset # 继续聊聊 bitset，bitset 中每个数子用一个 bit 即能表示，对于一个 int8 的数字，我们可以用它表示 8 个数字，能帮助我们大大节省数据的存储空间。\nbitset 最常见的应用有 bitmap 和 flag，即位图和标志位。这里，我们先尝试用它表示一些操作的标志位。比如某个场景，我们需要三个 flag 分别表示权限1、权限2和权限3，而且几个权限可以共存。我们可以分别用三个常量 F1、F2、F3 表示位 Mask。\n示例代码如下（引用自文章 Bitmasks, bitsets and flags）：\ntype Bits uint8 const ( F0 Bits = 1 \u0026lt;\u0026lt; iota F1 F2 ) func Set(b, flag Bits) Bits { return b | flag } func Clear(b, flag Bits) Bits { return b \u0026amp;^ flag } func Toggle(b, flag Bits) Bits { return b ^ flag } func Has(b, flag Bits) bool { return b\u0026amp;flag != 0 } func main() { var b Bits b = Set(b, F0) b = Toggle(b, F2) for i, flag := range []Bits{F0, F1, F2} { fmt.Println(i, Has(b, flag)) } } 例子中，我们本来需要三个数才能表示这三个标志，但现在通过一个 uint8 就可以。bitset 的一些操作，如设置 Set、清除 Clear、切换 Toggle、检查 Has 通过位运算就可以实现，而且非常高效。\nbitset 对集合操作有着天然的优势，直接通过位运算符便可实现。比如交集、并集、和差集，示例如下：\n交集：a \u0026amp; b 并集：a | b 差集：a \u0026amp; (~b) 底层的语言、库、框架常会使用这种方式设置标志位。\n以上的例子中只展示了少量数据的处理方式，uint8 占 8 bit 空间，只能表示 8 个数字。那大数据场景能否可以使用这套思路呢？\n我们可以把 bitset 和 Go 中的切片结合起来，重新定义 Bits 类型，如下：\ntype Bitset struct { data []int64 } 但如此也会产生一些问题，设置 bit，我们怎么知道它在哪里呢？仔细想想，这个位置信息包含两部分，即保存该 bit 的数在切片索引位置和该 bit 在数字中的哪位，分别将它们命名为 index 和 position。那怎么获取？\nindex 可以通过整除获取，比如我们想知道表示 65 的 bit 在切片的哪个 index，通过 65 / 64 即可获得，如果为了高效，也可以用位运算实现，即用移位替换除法，比如 65 \u0026raquo; 6，6 表示移位偏移，即 2^n = 64 的 n。\npostion 是除法的余数，我们可以通过模运算获得，比如 65 % 64 = 1，同样为了效率，也有相应的位运算实现，比如 65 \u0026amp; 0b00111111，即 65 \u0026amp; 63。\n一个简单例子，如下：\npackage main import ( \u0026#34;fmt\u0026#34; ) const ( shift = 6 mask = 0x3f // 即0b00111111 ) type Bitset struct { data []int64 } func NewBitSet(n int) *Bitset { // 获取位置信息 index := n \u0026gt;\u0026gt; shift set := \u0026amp;Bitset{ data: make([]int64, index+1), } // 根据 n 设置 bitset set.data[index] |= 1 \u0026lt;\u0026lt; uint(n\u0026amp;mask) return set } func (set *Bitset) Contains(n int) bool { // 获取位置信息 index := n \u0026gt;\u0026gt; shift return set.data[index]\u0026amp;(1\u0026lt;\u0026lt;uint(n\u0026amp;mask)) != 0 } func main() { set := NewBitSet(65) fmt.Println(\u0026#34;set contains 65\u0026#34;, set.Contains(65)) fmt.Println(\u0026#34;set contains 64\u0026#34;, set.Contains(64)) } 输出结果\nset contains 65 true set contains 64 false 以上的例子功能很简单，只是为了演示，只有创建 bitset 和 contains 两个功能，其他诸如添加、删除、不同 bitset 间的交、并、差还没有实现。有兴趣的朋友可以继续尝试。\n其实，bitset 包也有人实现了，github地址 bit。可以读读它的源码，实现思路和上面介绍差不多。\n下面是一个使用案例。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/yourbasic/bit\u0026#34; ) func main() { s := bit.New(2, 3, 4, 65, 128) fmt.Println(\u0026#34;s contains 65\u0026#34;, s.Contains(65)) fmt.Println(\u0026#34;s contains 15\u0026#34;, s.Contains(15)) s.Add(15) fmt.Println(\u0026#34;s contains 15\u0026#34;, s.Contains(15)) fmt.Println(\u0026#34;next 20 is \u0026#34;, s.Next(20)) fmt.Println(\u0026#34;prev 20 is \u0026#34;, s.Prev(20)) s2 := bit.New(10, 22, 30) s3 := s.Or(s2) fmt.Println(\u0026#34;next 20 is \u0026#34;, s3.Next(20)) s3.Visit(func(n int) bool { fmt.Println(n) return false // 返回 true 表示终止遍历 }) } 执行结果：\ns contains 65 true s contains 15 false s contains 15 true next 20 is 65 prev 20 is 15 next 20 is 22 2 3 4 10 15 22 30 65 128 代码的意思很好理解，就是一些增删改查和集合的操作。要注意的是，bitset 和前面的 set 的区别，bitset 的成员只能是 int 整型，没有 set 灵活。平时的使用场景也比较少，主要用在对效率和存储空间要求较高的场景。\n总结 # 本文介绍了Go 中两种 set 的实现原理，并在此基础介绍了对应于它们的两个包简单使用。我觉得，通过这篇文章，Go 中 set 的使用，基本都可以搞定了。\n除这两个包，再补充两个，zoumo/goset 和 github.com/willf/bitset。\n博文地址：[https://www.poloxue.com/posts/2019-06-03-set-in-golang/]\n","date":"2019-06-03","externalUrl":null,"permalink":"/posts/2019-06-03-set-in-golang/","section":"文章","summary":"在Go编程中，数据结构的选择对解决问题至关重要。本文将探讨如何使用set和bitset两种数据结构，以及它们在Go中的应用场景。\nGo 的数据结构 # Go 内置的数据结构并不多。工作中，我们最常用的两种数据结构分别是 slice 和 map，即切片和映射。 其实，Go 中也有数组，切片的底层就是数组，只不过因为切片的存在，我们平时很少使用它。\n","title":"Golang 中如何使用 Set","type":"posts"},{"content":"本文谈下我对 Go 版本管理的一些想法。让后，我将介绍一个小工具，gvm。这个话题说起来也很简单，但如果想用的爽，还是要稍微梳理下。\n背景介绍 # Go 的版本管理，并非包的依赖管理，而且关于如何在不同的 Go 版本之间切换。平时的工作中，正常情况，我们不会遇到这样的需求，所以可能并不明白它的价值。\n简单说下我写这篇文章的背景吧。\n最近几周，Go 最重要的一则消息应该莫过 9月份 Go 1.13 的正式发布。它的相关升级可查看 Go 1.13 正式发布，看看都有哪些值得关注的特性 或官方 Go 1.13 Relase Notes。\n对于一名 gopher 而言，可能早已按捺不住自己那颗躁动的心，想尽快体验下新版的升级项。但问题是，切换至新版 Go 通常会遇到一些问题，比如不同版本的环境配置，安装的辅助工具和程序包在不同版本下可能会存在兼容或被覆盖等问题。\n我自然就希望有一套方案可以帮助我完成 Go 版本的切换，实现不同版本间环境的完全隔离。\n思考方案 # 谈到环境隔离，有很多方案可供选择，如多主机、虚拟机、容器等技术。这些听起来都挺不错，都能实现需求。但如果只是为了 Go 版本管理，完全可以自己实现。\n多版本切换，主要是不同版本环境变量的隔离。Go 1.10 之前，我们关心的变量有 GOROOT、GOPATH 和 PATH。Go 1.10 之后，GOROOT 已经默认为 go 的当前安装路径，只要考虑 GOPATH 和 PATH 即可。\n最近，刚答过一个关于 Go 环境变量的问题，查看回答。其中对每个变量的作用进行了比较细致的描述。\n如何实现 # 现在，我要实现我自己电脑上的两个版本的 Go 自由切换，该如何做呢？\n假设它们分别位于 ~/.goversions/sdk/ 目录下的 go1.11/ 和 go1.13/。我现在要启用 go 1.11，运行如下命令即可：\n$ export PATH=~/.goversions/sdk/go1.11/bin/:$PATH 此时，GOROOT 已经自动识别，为 ~/.goversions/sdk/go1.11/。Go 相关的工具链，源码，标准库都在这个目录下。\n但除 Go 本身相关的，还有其他第三方标准库、编译生成的库文件等内容，它们都位于 GOPATH 下，如果不设置，默认为 ~/go，在切换多版本的时候，就会产生混乱。我们可以为每个版本单独设置个 GOPATH。\n如 go1.11，设置 GOPATH 为 ~/.goversions/gopath/go1.11-global/。\n$ mkdir ~/.goversions/gopath/go1.11-global/ $ export GOPATH=~/.goversions/gopath/go1.11-global/ 一个独立的环境创建好了。\n如果现在要切换至 go 1.13，几个命令即可搞定。\n$ export PATH=~/.goversions/sdk/go1.13/bin/:$PATH $ mkdir -pv ~/.goversions/gopath/go1.13-global/ $ export GOPATH=~/.goversions/gopath/go1.13-global/ 切换成功。\n虽然，已经实现了需求，但总觉得用起来非常不爽。为了操作方便，其实可以把上面的思路提炼成 shell 脚本，整理成一套工具。\n是不是蠢蠢欲动，想试一下？\n但很遗憾，已经没这个机会了，因为这个工具已经有人开发了，思路类似，但却比这里描述的要强，它就是 gvm, 地址 moovweb/gvm。\n什么是 gvm # gvm，即 Go Version Manager，Go 版本管理器，它可以非常轻量的切换 Go 版本。对比其他语言，通常也有类似的工具，如 NodeJS 的 NVM，Python 的 virtualenv 等。\ngvm 不仅包含上面提到的版本切换，还可以直接通过源码编辑安装任意版本的 Go，当然最好是 1.5 及之后版本，原因后面解释。\n一件比较尴尬的点，gvm 产生背景并非是为了 Go 在不同版本间的切换，开发团队当初开发这个工具主要为了解决项目的依赖问题，通过切换环境实现包依赖的切换。下面，我会演示如何做到这一点。\n但问题是，现在 Go 的依赖管理已经日趋完善，官方的 go module 也越来越好用，GOPATH 在被逐渐弱化，gvm 似乎也就只剩下帮我们快速体验不同 Go 版本的功能还有点价值。\n废话说了那么多，开始正式体验下这个工具吧。\n如何安装 # 安装很简单，只要如下一行命令即可搞定。\n$ bash \u0026lt; \u0026lt;(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) 输出显示：\nCloning from https://github.com/moovweb/gvm.git to /home/vagrant/.gvm which: no go in (/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/vagrant/.local/bin:/home/vagrant/bin) No existing Go versions detected Installed gvm v1.0.22 Please restart your terminal session or to get started right away run `source /home/vagrant/.gvm/scripts/gvm` 安装完成！\n重启控制台或执行 source $HOME/.gvm/scripts/gvm 即可启用 gvm。\n提醒下，不同操作系统还需要相应的依赖项要装，具体查看 项目说明 的介绍。 这里面没有提到 Windows，不知道可不可用。\ngvm 安装 Go # gvm 通过从 github 下载源码编译 Go 的安装。而版本则是基于源码中的 tag。因为 1.5 版本及之后，Go 已经实现了自编译，因而要使用 gvm 安装 Go，我们要提前有可用的 Go 环境。\n好在，gvm 也提供直接通过下载二进制包的方式安装 Go。\n❯ gvm install go1.19 --binary Installing go1.19 from binary source Go 安装完成，就可以使用 gvm 随意安装切换任意版本的 Go 了。\n$ gvm install go1.11 等待运行完成即可。\n首次安装的时间可能会比较久，主要取决于你的网络，因为第一次需要从 github 下载源码。\n查看版本 # 首先，查看下我的系统已经安装哪些 Go 版本有哪些吧，相关命令 gvm list。\n$ gvm list gvm gos (installed) go1.11 go1.12 go1.13 go1.13beta1 安装了 4 个版本，其中，go1.13beta1 是非稳定版本，所以说，如果我们想尽快尝试 go 的新特性，gvm 还是很便捷的。\n除了查看已安装的版本，还可以通过 gvm listall 查看所有版本，版本来源于源码中的 tag 标签。\n$ gvm listall gvm gos (available) go1 go1.0.1 go1.0.2 go1.0.3 go1.1 ... go1.13 go1.13beta1 go1.13rc1 go1.13rc2 但这个操作在 mac 上无法执行，gvm 的实现中用到了 Linux 的 sort 命令，它与 mac 上的 sort 不兼容。\n怎么解决？\n安装个软件 coreutils， 它之中有个 qsort 命令可用。通过 brew install coreutils 可直接安装。然后，修改下文件 $HOME/.gvm/scripts/function/tool，将其中的 sort 修改为 qsort 即可。\n选择版本 # 选择启用的版本就非常简单了。如下：\n$ gvm use go1.11 [--default] 启用成功后，可以通过 go version 和 go env 确认下。如果想默认一个版本，加上 \u0026ndash;default 设置即可。\n包环境管理 # gvm 除了 Go 版本的管理，还可以管理包环境，相关命令是 pkgenv 和 pkgset。如果没使用包依赖管理工具，它也是挺方便的。\n演示个例子，假设我们要创建一个新的项目 blog，可提前创建相应的环境。\n$ gvm pkgset create blog # 创建 $ gvm pkgset use blog # 启用 闲杂，我们通过 go get 安装的包都会默认在 blog 环境下。基于的原理是 go get 默认会把安装的放在 GOPATH 中的第一个目录下。\n好了，就介绍这么多吧。有兴趣的朋友可以再研究研究。毕竟在有了 go mod 之后，这个功能以后是基本不会用了。\ngvm 目录结构 # gvm 是 shell 编写，默认是安装在 $HOME/.gvm/ 目录下。查看下它的目录结构会有助我们了解它的实现。\n其中几个主要的目录，如下：\narchive # go 源码 bin # gvm 可执行文件 environments # 不同环境的环境变量配置 scripts # gvm 的子命令脚本 logs # 日志信息 pkgsets # 每个独立环境 gopath 所在路径 在研究了 gvm 的实现后，我们会发现，这一套思路其实也适用于其他很多工具的版本管理。如果之后再遇到同样的需求，即使我们没有现成的工具，自己实现一套也是可以的。\n总结 # 本文从我的需求出发，引出了如何灵活地进行管理 Go 版本的话题。\n以往的经验告诉我，既然其他语言都有工具实现这样的需求，Go 也应该有。搜索了下，找到了 gvm。虽说我在使用它的时候，发现了一些 bug 与体验不好的地方，但总体而言，已经足够满足我的需求。\n参考 # Go 语言多版本安装及管理利器 - gvm moovweb/gvm gvm + go mod\n博文地址：Go 虚拟环境管理工具 gvm\n","date":"2019-05-27","externalUrl":null,"permalink":"/posts/2019-05-27-golang-virtualenv-tool-gvm/","section":"文章","summary":"本文谈下我对 Go 版本管理的一些想法。让后，我将介绍一个小工具，gvm。这个话题说起来也很简单，但如果想用的爽，还是要稍微梳理下。\n背景介绍 # Go 的版本管理，并非包的依赖管理，而且关于如何在不同的 Go 版本之间切换。平时的工作中，正常情况，我们不会遇到这样的需求，所以可能并不明白它的价值。\n","title":"Go 虚拟环境管理工具 gvm","type":"posts"},{"content":"经过前面的一系列工作后，Go 的语言环境已经搭建完成。\n我们初步体验了 Go 提供的大部分命令。但在正式进入开发之前，还有件工作要做，那就是选择一款适合自己的 IDE。\n为什么使用IDE # \u0026ldquo;程序员为什么要使用 IDE\u0026rdquo;，在一些社区论坛，经常可以看到这样的提问。关于是否应该使用IDE，每个人都有着自己的看法。\n早期，程序的开发并不需要 IDE，那是以机器码编程为主的时代。后来随着计算机行业发展，为了进一步提升工程开发效率，IDE就产生了。\n要明白的是，IDE主要是通过把各类命令工具集整合起来，开发的一套易于程序开发的软件，通常它帮我们形成一套高效的编程开发习惯。最终目标是为了提升项目的开发效率。\n了解了 IDE 的本质，如果喜欢折腾，我们完全可以把诸如 vim 或 emacs 等文本编辑器打造一款属于自己的IDE。\n支持哪些功能 # 无论用的是市面上已有的 IDE，还是 vim 纯手动打造的IDE，都离不开一个话题：IDE 涉及的功能有哪些？文本编辑的能力就不必介绍了，它是最基本的功能。\n快捷键 # 双手不离键盘是高效开发中非常重要的一点，要做到它，我们就需要依赖功能强大的快捷键。IDE 通常都有一套独有的快捷键规范。当习惯了一款 IDE，快捷键或许是大家轻易不愿更换 IDE 的重要原因之一。\n代码高亮 # 代码高亮主要涉及变量、函数定义、类、常量、特殊符号、关键词等。代码高亮可以提高代码阅读体验，对不同语法采用不同的配色方案，也可降低代码错误的发生几率。而且，IDE一般都支持自定义配色，可以由个人爱好自由设置。\n代码格式化 # 为了方便团队开发，在项目开发前，通常都会制定统一的代码规范。制定好的规范需要遵从，而 IDE 一般都支持代码的格式化功能，帮我们更方便地实现目标。需要说明的是，不同于 Go，很多编程语言并没有类似 gofmt 的命令，代码规范也是多样。\n代码提示 # IDE的代码提示能根据输入快速给出一系列的建议列表，比如参数信息、成员列表、代码片段等。为了给出更精准的提示，一些IDE可能甚至会分析用户历史的操作记录。感觉这俨然已经是一个小型的推荐系统了。\n导航跳转 # 大型项目的代码量通常较大，涉及文件也较多。在开发时，我们经常需要在变量、函数、类等代码间跳转。最不便利的方式，我们可以通过键盘方向键或鼠标实现切换。IDE通常都实现了在变量、类型定义、函数定义、文件之间快速跳转的方法。\n代码调试 # 多数情况下，通过打印函数就可以实现代码调试。但通过系统化工具提供的调试功能，我们就能应付各种复杂的场景。调试工具通常支持各种断点调试能力、变量观察等功能。\n构建编译 # Linux 下最常用的构建工具应该是 Makefile，之前开发C/C++用的便是make。但有些语言项目用它构建会很复杂，比如 Java。IDE 的构建编译功能可以快捷地生成目标文件。编译功能通常使用的是语言自带编译器，比如 Go 用 go build 命令。\n其他功能 # 当然，除上面介绍的这些，IDE可能还有很多其他能力，比如代码重构、文件历史记录、语言环境管理、数据库管理等。只要是能想到的功能，基本都可集成进来，现在的 IDE 俨然已经完全超出了传统IDE的范畴。\nGO有哪些IDE # GO的发展已有十几年之久。在这期间出现了很多能编写 GO 语言的 IDE，把它们都详细介绍一遍是不现实的。接下来，重点介绍我比较了解几款IDE。\nGoland # Goland，商业公司 jetbrains 推出的 Go 集成开发环境，它真的是无比强大。\n我相信很多程序都用过他们家的IDE，比如Java的 Intellj IDEA、PHP的PHPStorm、Python的PyCharm、C++的CLion、前端的WebStorm等。使用JetBrains的IDE，我们可以享受到它优秀的开箱即用的体验和 jetbrains 积累十几年的插件体系。\n前些年，也就是Goland发布之前，如果我们希望用jetbrain的IDE进行GO的开发，需要通过它提供的插件支持。Goland发布后，这些插件似乎已经下架了。\n不得不承认，Goland的功能层面做的确实非常完美。不过有几点我想吐槽一下，首先必须要提的是，Jetbrians的IDE基本都存在着卡顿的毛病，资源消耗比较严重。虽然一些大牛提供了优化方案，但体验下来，和其他IDE依然没有相比。\nGoland 开箱即用，它的问题很少，确实没有多少可介绍的，安装完成基本就可以开始编码！\nVS Code # 由微软开发的一款功能强大的现代化轻量级代码编辑器IDE，免费开源。通过它强大的插件扩展能力，VS Code几乎支持主流语言的项目开发。毫无例外，GO也是其中之一。\n我之所以尝试VS Code，并非所谓的极客思维，喜欢瞎折腾。而是因为jetbrains的IDE经常会卡的心痛，而且自己经常会在不同语言间切换。一次启动多款Jetbrains的IDE，这是很痛苦的。\n为VS Code加入GO的开发能力，要安装一款插件即可，可参考 VsCode 的 Golang 文档。\n安装时，可能遇到一些问题，常见的就是，在安装一些依赖包时会出现网络下载失败。关于原因就不说了，大家都明白。不过，问题还是要解决的。举个具体的例子吧！在GO插件时，我们会通过go get golang.org/x/tools/xxx安装某个包，这时候大概率出现网络连接错误。我们可以通从github找到对应的仓库，golang/tools，然后使用git命令下载后，放在GOPATH指定的目录下，然后再安装即可。\n插个题外话，VS Code使用的是Electron开发的，Electron是用HTML，CSS和JavaScript来构建跨平台桌面应用程序的一个开源库，NodeJS与Chromium的结合。因此，利用浏览器的特性，利用VS Code，我们能实现很多奇葩的插件。\nGitHub Daily：装上这几个 VSCode 插件后，上班划水摸鱼不是梦\nVim GO # 细究起来，vim 是一款文本编辑器，但它却拥有了很多不该属于文本编辑器的能力，比如单词补全、ctags标签跳转、窗口分隔、崩溃文件恢复、文件diff、400多种文本高亮等。最重要的一点是，vim有一套自己的脚本语言，这为它通过插件扩展自己的能力提供了可能。\n将vim扩展成一款适合自己使用的GO IDE，不仅要编写许多复杂的配置与脚本，还需要各种插件的相互配合，才能实现我们的目标。比如前面介绍的那些IDE的常见功能，在vim中都要逐一配置实现。\nGO的vim环境搭建，需要用到一款非常重要的插件，vim-go，youtube上还有他的分享视频，有兴趣可以去看看。\nvim-go提供了诸如代码的编译、执行、测试、代码重构、错误提示等各种功能，具体了解可查看 vim-go教程。\n说明一点，虽然 vim 支持插件扩展，但它要集成出 VS Code 的体验还是非常困难的。\n当前我用的主要就是这三款IDE，Goland VSC 和 vim。当然，还有很多其他IDE，下面也简单介绍下，但因为没怎么使用过，所以很难有经验之谈了。\n最近将 vim 迁移到了 Neovim，体验下来，真的是一级棒。\nSublime Text # 最初用VS Code，感觉它的使用习惯和Sublime相似。但说到Sublime，都说它是强大文本编辑器，而它的编码能力也是插件扩展来的。GoSublime就是为Sublime扩展GO功能的插件。\nLiteIDE # 一款轻量级的IDE，听说是由中国人开发的。可能在Goland出现之前比较流行。也或许是自己孤陋寡闻，不知道现在还有多少人在用。\nEclipse # 开源的IDE，盛行了多年，有着丰富的资源和粉丝人群，应该是Java开发最喜欢的IDE吧。GoEclise是Eclipse针对Goland的插件。从github了解到，这个项目好像很久没有更新了。\nAtom # 与VS Code一样，都是基于Node-Webkit，即Electron，开发的。是由github开源的文本编辑器。go-plus是Atom针对Golang开发的插件\n总结 # 本篇文章从为什么要使用IDE谈起，介绍了IDE的一些发展史。同时，总结了一款基本的IDE通常都会提供哪些功能。只要了解了这些，可以帮助我们以后更好地使用它们。最后，介绍了现在市面上流行的几款IDE，并在我力所能及的范围内分析了它们各自的优劣。\n","date":"2019-05-06","externalUrl":null,"permalink":"/posts/2019-05-06-golang-ide/","section":"文章","summary":"经过前面的一系列工作后，Go 的语言环境已经搭建完成。\n我们初步体验了 Go 提供的大部分命令。但在正式进入开发之前，还有件工作要做，那就是选择一款适合自己的 IDE。\n","title":"Go 的那些 IDE","type":"posts"},{"content":"利用 go run 和 go build 可以完成 Go 的整个编译执行流程。但 Go 提供给我们的命令当然远不止这两个。\n本文将在所能及的范围内，尽量地介绍GO提供的所有命令，从而实现对它们有个整体的认识。\n概述 # 除了 gofmt 与 godoc 外，Go 中的命令一般都可通过go命令调用，这些命令可理解为go的子命令，查看下命令列表，如下：\n$ go Go is a tool for managing Go source code. Go是管理Golang源码的工具 Usage: 使用方式： go \u0026lt;command\u0026gt; [arguments] go \u0026lt;命令\u0026gt; [参数] The commands are: 涉及的命令包括： bug start a bug report 提交bug报告，执行后会开启浏览器并转到github的issue，当前的配置与环境都会自动填写到issue中 build compile packages and dependencies 编译源码和依赖包 clean remove object files and cached files 清理文件，如测试与编译中生成或缓存的文件 doc show documentation for package or symbol 可用于显示包、接口和函数等的文档 env print Go environment information 打印当前的环境变量信息 fix update packages to use new APIs 可用于go新旧版本之间的代码迁移，修正代码兼容问题 fmt gofmt (reformat) package sources 按规范格式化源码 generate generate Go files by processing source 扫描源码注释，类似//go:generate command argument...实现生成go文件 get download and install packages and dependencies 下载并安装包和依赖 install compile and install packages and dependencies 编译并安装包和依赖 list list packages or modules 列出包或模块的信息 mod module maintenance 用于模块的管理维护 run compile and run Go program 编译与执行Go程序 test test packages 测试包 tool run specified go tool 运行go提供的一些特定工具，比如pprof version print Go version 打印go版本信息 vet report likely mistakes in packages 检查与报告代码包中的错误 ... 输出的介绍大致翻译了下，其中部分命令也作了些介绍。除了go的子命令，go tool下也有些更底层的命令，执行go tool即可查看：\n$ go tool addr2line 可以调用栈的地址转化为文件和行号 asm 和汇编有关的命令，没搞清楚如何使用 buildid 似乎用在编译时，根据文件内容生成hash cgo 可帮助我们实现在GO中调用C语言代码 compile 用于编译源码生成.o文件 cover 可用于分析测试覆盖率 dist 帮助引导、构建和测试go doc 测试下来似乎和go doc效果一样，都是用于文章管理 fix 用于解决不同版本间代码不兼容问题，和go fix作用一样 link 用于库的链接 nm 可列出如对象文件.o，可执行文件或.a库文件中的函数变量符号等信息 objdump 反汇编命令 pack 似乎是个打包压缩命令 pprof 自带的性能分析工具 test2json 用于把测试文件转化可读的json格式 tour 启动本地的tour教程，可见GO团队真的很用心 trace 可用于问题诊断与调式的工具 go tool的输出默认没有任何文字说明，这里的介绍是我收集总结出来的，可能有些错误。 总体而言，我把GO中命令分为几个大类：\n源码编译 包的管理 代码规范 测试相关 调试优化 其他命令 GO的命令很多，很难在一篇文章中把每个都介绍清楚。接下来只做些简单演示说明，详细的介绍待以后有了具体场景在详细说明。\n源码编译 # go build、go run 两个命令可归于源码编译类的命令，是入门学习首先要掌握的，故而在前篇文章已经做了详细介绍。\ngo build用于编译可执行文件和库文件，参数可以是源码目录、.go文件。演示如下：\n$ go build # 目录 $ go build main.go # .go文件 $ go build main.go math.go data.go # .go文件列表 go run和build有很多相似点，它们都会编译源码。不同的是，go run只能用于可执行源码（即main包源码）的编译。接收参数为.go文件。演示如下：\n$ go run main.go $ go run main.go math.go data.go 详细了解，可以看下 详解GO的编译执行流程 这篇介绍。\n关于编译，go tool还有一些更细节的命令，比如compile、link等。有兴趣可以去了解下。\n包的管理 # GO的包管理是由语言包自带，这点不同于其他语言，如Java、Python、PHP等。从当前我所了解的来看，GO没有提供包管理仓库，而是直接使用的版本管理系统作为仓库，支持如git、svn、mercurial等。而用于包管理的命令有go install、go get、go list等。\ngo install用于源码包的编译与安装。虽然这里也涉及到编译，但从名字就可以看出，该命令重在强调安装。之前build命令在编译非main包会生成缓存文件，main包会生成执行文件并拷贝到当前目录。而install会将它们安装到指定的文件。假设有名为math的包，执行如下命令：\n$ go install math # 非main包，最终生成文件 $GOPATH/pkg/xxx/xxx/math.a。 $ go install entry # 是main包，最终生成文件 $GOBIN/entry go get用于从互联网安装更新包和依赖，类似于其他语言包管理器的install。不同于install，它多出网络下载这步，大概可理解为 go get 等价于 git/svn等下载 + git install。我们可以演示下从http://github.com/PuerkitoBio/goquery下载安装goquery的过程，如下：\n$ go get -v github.com/PuerkitoBio/goquery golang.org/x/net/html/atom golang.org/x/net/html github.com/andybalholm/cascadia github.com/PuerkitoBio/goquery 从上面可以看出，go get不仅下载了goquery，还下载了相应的依赖。执行完成后，GOPATH目录下可以找到goquery的源码与编译后的.a库文件。\ngo list可用于输出包的信息，接口的参数和在源码中import的路径相同，下面演示一些案例：\n$ go list fmt # 查看某包 fmt $ go list fmt net/http # 查看多个包 fmt net/http $ go list --json fmt # 查看包的具体信息 { \u0026#34;Dir\u0026#34;: \u0026#34;/usr/local/go/src/fmt\u0026#34;, \u0026#34;ImportPath\u0026#34;: \u0026#34;fmt\u0026#34;, \u0026#34;Name\u0026#34;: \u0026#34;fmt\u0026#34;, \u0026#34;Doc\u0026#34;: \u0026#34;Package fmt implements formatted I/O with functions analogous to C\u0026#39;s printf and scanf.\u0026#34;, \u0026#34;Target\u0026#34;: \u0026#34;/usr/local/go/pkg/darwin_amd64/fmt.a\u0026#34;, \u0026#34;Root\u0026#34;: \u0026#34;/usr/local/go\u0026#34;, \u0026#34;Match\u0026#34;: [ \u0026#34;fmt\u0026#34; ], \u0026#34;Goroot\u0026#34;: true, \u0026#34;Standard\u0026#34;: true, \u0026#34;GoFiles\u0026#34;: [ \u0026#34;doc.go\u0026#34;, ... \u0026#34;scan.go\u0026#34; ], \u0026#34;Imports\u0026#34;: [ \u0026#34;errors\u0026#34;, ... \u0026#34;unicode/utf8\u0026#34; ], \u0026#34;Deps\u0026#34;: [ \u0026#34;errors\u0026#34;, ... \u0026#34;unicode/utf8\u0026#34;, \u0026#34;unsafe\u0026#34; ], \u0026#34;TestGoFiles\u0026#34;: [ \u0026#34;export_test.go\u0026#34; ], \u0026#34;XTestGoFiles\u0026#34;: [ \u0026#34;example_test.go\u0026#34;, ... \u0026#34;stringer_test.go\u0026#34; ], \u0026#34;XTestImports\u0026#34;: [ \u0026#34;bufio\u0026#34;, ... \u0026#34;unicode/utf8\u0026#34; ] } 详情信息部分展示内容比较多，包含源码路径、导入路径、依赖了哪些包等一系列信息。\n代码规范 # 这类命令可以帮助我们规范代码的格式，减少代码发生错误的几率，其中主要有go fmt、go vet和go fix三个命令。\ngo fmt的作用是代码的格式化。为了让我们把更多时间花在开发工作上，GO官方制定了标准的代码规范并go fmt实现规范代码。假设有main.go文件内容如下：\npackage main func main() { a := x + y } 我们只需执行go fmt main.go文件，然后再次打开main.go文件：\npackage main func main() { a := x + y } 格式化已经完成。关于代码格式化还有一个更具体的命令：gofmt，go fmt是它的某个特殊形式：gofmt -l -w。\ngo vet是一个用于检查GO语言静态语法的工具。GO语言的语法是非常严格的，如不能定义未使用的变量、变量类型必须显式转化等等。示例，假设有main.go文件内容如下：\npackage main func main() { a := 1 + 2 b := 1 } 使用go vet执行源码检查，输出结果如下：\n$ go vet main.go # command-line-arguments [command-line-arguments.test] ./main.go:4:5: a declared and not used ./main.go:5:5: b declared and not used 我们被告知，变量a和b声明但并没有使用。\ngo fix主要用于处理代码的兼容性问题，例如go1之前老版本的代码转化到go1。但是有点遗憾的是，没找到该命令的演示案例。我们平时应该很少用到。\n测试相关 # GO也提供与相关的命令，为我们提供了一条方便验证我们代码的途径。与测试有关的命令有go test、go tool cover 和 go tool test2json。\ngo test可用于运行测试代码，以此验证程序逻辑的正确性能。具体演示下，示例代码包含两部分，分别是功能代码和测试代码。功能代码在math.go文件中，如下：\npackage math func Add(x, y int) int { return x + y } 测试用例的代码在math_test.go文件中，如下：\npackage math import \u0026#34;testing\u0026#34; func Test_Add(t *testing.T) { r := Add(1, 2) if r != 3 { t.FailNow() } } 接下来我们可以执行go test命令启动测试用例，如下：\n$ go test math_test.go math.go ok command-line-arguments 结果显示，测试执行成功，Add函数功能正常。我们可以把测试代码编译成可执行文件，如下：\n$ go test math_test.go math.go -o math.test 查看下会发现此时目录下多出了编译好的math.test可执行测试文件。\ngo cover可用于分析测试覆盖率。比如上面的测试案例，我们可以生成覆盖率文件，如下：\n$ go test *.go -coverprofile=coverage.out go cover 提供多种方式分析测试覆盖率，这里演示下如何用html展示测试结果，如下：\n$ go tool cover --html=size_coverage.out 显示测试覆盖率为100%。我们这里的测试用例比较简单，所以到达了全面覆盖。\ngo tool test2json可用于将go test测试结果转化为json格式。这里需要使用之前生成的测试执行文件，示例如下：\n$ go tool test2json ./math.test -test.v {\u0026#34;Action\u0026#34;:\u0026#34;run\u0026#34;,\u0026#34;Test\u0026#34;:\u0026#34;Test_Add\u0026#34;} {\u0026#34;Action\u0026#34;:\u0026#34;output\u0026#34;,\u0026#34;Test\u0026#34;:\u0026#34;Test_Add\u0026#34;,\u0026#34;Output\u0026#34;:\u0026#34;=== RUN Test_Add\\n\u0026#34;} {\u0026#34;Action\u0026#34;:\u0026#34;output\u0026#34;,\u0026#34;Test\u0026#34;:\u0026#34;Test_Add\u0026#34;,\u0026#34;Output\u0026#34;:\u0026#34;--- PASS: Test_Add (0.00s)\\n\u0026#34;} {\u0026#34;Action\u0026#34;:\u0026#34;pass\u0026#34;,\u0026#34;Test\u0026#34;:\u0026#34;Test_Add\u0026#34;} {\u0026#34;Action\u0026#34;:\u0026#34;output\u0026#34;,\u0026#34;Output\u0026#34;:\u0026#34;PASS\\n\u0026#34;} {\u0026#34;Action\u0026#34;:\u0026#34;pass\u0026#34;} 测试结果以json格式打印出来。虽然我对专业的测试不太了解，但是也明白结构化的输出是比较容易程序化分析的。\n调试优化 # 完成代码开发后，可能时刻会遇到一些bug或者性能问题。GO提供了go tool pprof、go tool trace、go tool addr2line和go tool nm等一系列命令，可用于代码调试优化。\ngo tool pprof可用于帮助我们分析程序收集的性能数据，比如CPU、内存等数据。以官方提供的示例为例吧，博客地址在 博客。示例代码在benchgraffiti。\ngo tool trace 可用于追踪程序执行情况。go tool pprof可以通过cpu和内存数据分析出程序的瓶颈。\ngo tool addr2line可以将地址转化对应源码的文件和行号，非常方便的便于我们调式问题。\n具体的案例就不演示了。这部分的命令稍微有点复杂，待后面有了具体案例再来补充。\n其他命令 # GO中还提供了很多辅助命令。这些命令有go bug、go doc、go tool tourl等。\ngo bug会直接启动浏览器并进入github的go项目的issue之下，还会把用户当前环境信息自动添加到issue中。如下执行go bug之后跳转的页面：\n由此可见，GO的开发团队真的非常用心，做了很多简化我们工作的事情。\ngo doc为我们提供了快速查看文档的途径，比如查看fmt文档，我们只需执行go doc fmt，fmt相关的文档便会输出到控制台。我们也可以像官网文档那样用浏览器查看文档，只需执行godoc -http=:6060，便会启动一个本地的web服务。我们访问localhost:6060就能看到一个几乎和官网一样的页面，示例如下：\ngo tool tourl是官方提供的本地搭建tour教程的方式。我们只需执行go tool tour便会自动启动浏览器并进入教程首页。\n到这里我们可以发现，即使由于一些原因使我们无法访问GO的官网，但有了这些工具，我们也可以愉快地进行GO的学习。\n总结 # 本篇文章以GO命令的快速体验为目标，概要式地介绍了几乎所有的命令。在对它们有了基本认识后，在以后遇到问题时，我们才能想到它们，以及更快地掌握和使用它们。\n","date":"2019-04-29","externalUrl":null,"permalink":"/posts/2019-04-29-golang-commandline/","section":"文章","summary":"利用 go run 和 go build 可以完成 Go 的整个编译执行流程。但 Go 提供给我们的命令当然远不止这两个。\n本文将在所能及的范围内，尽量地介绍GO提供的所有命令，从而实现对它们有个整体的认识。\n","title":"Go 命令快速体验","type":"posts"},{"content":"本篇文章进入 Go 的开发环境搭建系列。\n我们知道，编写任何语言的代码都离不开两样工具，语言开发包和代码编辑工具。\n今天先来聊聊如何安装 Go。\n我们或许都会觉得这种事非常简单，不值得写篇文章介绍。最初我也是这么想的。但深入了解下来，渐渐感觉这也是一件很有意思的事情。\n如何安装 # 和其他语言的安装类似，Go 的安装我们也可以采用三种方式进行，从简单到复杂依次是通过系统方式安装、官方二进制包安装和源码编译安装。\n系统方式 # 不同操作系统通常都会为 Go 提供相应的软件安装方式。这种方式很大程度上简化了安装过程，能为我们省去一些繁杂的步骤。下面分别介绍下不同系统下的安装方式：\nwindows\n在windows下，软件安装通常可通过下载类似 setup.exe/msi 软件包来操作。按照导航的提示，不断执行 \u0026ldquo;下一步\u0026rdquo; \u0026ldquo;下一步\u0026rdquo; 即可完成。访问 下地地址 将看到如下内容：\n选择其中的 \u0026ldquo;Microsoft Windows\u0026rdquo; 下载 windows 安装包。现在的系统基本都是64位的了，一般情况下不用考虑 32/64 位系统的问题。\n下载好了安装包，点击启动执行，接下来的步骤就是按导航提示一步步操作即可。有一点要注意的是，GO的默认安装在 C:\\GO，如果要修改默认安装路径，在见到如下界面时重新选择。\nubuntu/debian\n在debian或ubuntu上，我们可使用 apt-get 命令安装go。比如，在Ubuntu 16.04.5 LTS系统，使用如下命令安装：\nsudo apt-get update // 视情况决定是否更新 sudo apt-get install golang-go 如果是新建的系统，建议先update下软件源。否则可能会因为某些源异常而无法顺利安装。\ncentos/redhat\n在centos或redhat上，我们可以使用yum命令安装go。比如，在CentOS 7.5上，使用如下命令安装：\n$ yum epel-release $ yum install golang 先下载了epel-releaes源，可防止出现yum安装golang不支持或版本太旧的问题。\nmacos\n在macos上，我们可使用pkg文件或homebrew安装go。\npkg的安装方式与windows的setup.exe/msi的类似，下载软件然后按导航 \u0026ldquo;下一步\u0026rdquo; \u0026ldquo;下一步\u0026rdquo; 即可完成。\n来说说如何使用homebrew安装。和yum和apt-get不同，homebrew并非mac系统自带，我们需要先安装。进入homebrew官网，页面顶部便说明了安装的方式，命令如下：\n/usr/bin/ruby -e \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\u0026#34; 接着安装go，命令如下：\n$ brew install go 非常简单就完成了安装。\n系统安装方式的优点是简单。缺点是我们不能保证系统提供给的版本一定能满足我们的要求，比如上面ubuntu安装的go版本就较低，为go1.6。\n二进制包 # 接下来说说如何使用二进制包安装。所谓二进制包，也就是已经编译好的包。这种安装方式在不同的操作系统上步骤类似，考虑到windows用户最多，就以windows为例吧。\n再次进入到下载页面，在列表可如下内容。因为我用的32位windows虚拟机，下载i386的包。\n接着把下载的压缩包解压到某个文件夹，比如 c:\\Program Files 下，进入查看，会发现其中已经包含了新的名为go的文件夹。\n至此，二进制包的方式安装就完成了。因为二进制包是已编译好的软件包，所以不同系统、CPU架构下需要下载与之相应的包。\n我们或许会想，就是移动个文件夹？是的，系统安装其实也就是做这些事情，不同在于系统安装在简化了操作的同时也会针对性做一些设置，比如配置好环境变量、文件分类存放等。\n源码编译 # 这种安装方式的好处是与系统无关，一切控制权都掌握在自己身上，能限制我们的只有自己的能力。\n上篇文章说过，go在1.5版本已经移除了源码中全部的C代码，实现了自编译。因此，我们可以用系统已有go来编译源码，从而实现新版的安装。\n前面在ubuntu下，我用apt-get安装的golang比较老的go1.6版。下面通过它来编译新版go。\n下载源码，最新版源码可点击 go1.12.2.src.tar.gz 下载。这里多说几句，go的源码托管在github上，地址：https://github.com/golang/go ，如想提前尝试新功能，可直接从github拉取最新的代码编译。这也是源码编译安装的一个好处。\n源码下载完成后进入源码目录即可编译。注意，如果用虚拟机编译，要保证有充足的内存。\n$ tar zxvf go1.12.2.src.tar.gz // 解压源码包 $ cd go/src $ ./all.sh 执行./all.sh即可完成编译安装，也挺简单的。这个过程会耗费一旦时间，要等待会。其实这里简化了很多细节，如果想仔细研究的话，可以去阅读官方文档 install go from source。\n环境变量 # 在安装完golang后，还需了解三个环境变量，分别是GOROOT、GOPATH、PATH。下面来分别介绍一下它们的作用。\nGOROOT # GO安装的根目录。该变量在不同的版本需要选择不同的处理方式。\n在 GO 1.10 之前，我们需要视不同安装方式决定是否手动配置。比如源码编译安装，安装时会有默认设置。而采用二进制包安装，在windows系统中，推荐安装位置为C:\\GO，在Linux、freeBSD、OS X系统，推荐安装在/usr/local/go下。如果要自定义安装位置，必须手动设置GOROOT。如果采用系统方式安装，这一切已经帮我们处理好了。\n关于这个话题，推荐阅读：you-dont-need-to-set-goroot和分析源码安装go的过程。\n在 GO 1.10 及以后，这个变量已经不用我们设置了，它会根据go工具集的位置，即相对go tool的位置来动态决定GOROOT的值。说简单点，其实就是go命令决定GOROOT的位置。\n关于这个话题，推荐阅读：use os.Executable to find GOROOT 和 github go issues 25002。\nPATH # 各个操作系统都存在的环境变量，用于指定系统可执行命令的默认查找路径，在不写完整路径情况下执行命令。\n以Windows为例，我之前把go安装在 C:\\Program Files\\go目录下，即GOROOT为C:\\Program Files\\go，那么PATH变量可追加上C:\\Program Files\\go\\bin。\nGOPATH # 如果有朋友了解python，可以将其类比为python的环境变量PYTHONPATH，用来设置我们的工作目录，即编写代码的地方。包也都是从GOPATH设置的路径中寻找。\n在go 1.8之前，该变量必须手动设置。go 1.8及之后，如果没有设置，默认在$HOME/go目录下，即你的用户目录中的go目录下。\n如何设置 # 介绍完三个变量，以我的mac为例介绍下设置方式吧。\n类unix系统环境变量的设置方式都类似。使用export命令设置环境变量，并将命令加入到/etc/profile，该文件会在开启shell控制台的情况下执行。具体操作命令如下：\n$ sudo vim /etc/profile ... export GOROOT=/usr/local/go // 默认位置可不用设置，1.10版本后也可以不设置 export PATH=$PATH:$GOROOT/bin export GOPATH=/Users/polo/work/go // 可设置多个目录 经过以上步骤，环境变量配置完成，如果要立刻启用环境变量，我们需要重启下控制台。接着我们可以用go env看一下变量的配置情况。\n$ go env GOARCH=\u0026#34;amd64\u0026#34; GOBIN=\u0026#34;/usr/local/go/bin\u0026#34; ... GOPATH=\u0026#34;/Users/polo/Public/Work/go\u0026#34; ... GOROOT=\u0026#34;/usr/local/go\u0026#34; 目录结构 # 再简单介绍下go的目录结构。以windows为例，进入C:\\Program Files\\go将看到如下内容。\n介绍几个比较主要的目录：\napi，里面包含所有API列表，好像IDE使用了里面的信息； bin，里面是一些go的工具命令，主要是go、gofmt、godoc，命令使用方法后面介绍 doc，go的使用文档，可以让我们在没有网络的情况下也可以阅读； src，主要是一些源码，如golang的编译器、各种工具集以及标准库的源码， 入门案例 # 介绍到这里已经差不多了，接着来写一个简单的例子，即经典的Hello World。\n首先，创建一个名为hello.go的文件，后缀必须为.go，内容如下：\npackage main import \u0026#34;fmt\u0026#34; func main(){ fmt.Println(\u0026#34;Hello World\u0026#34;) } 上面的代码主要由几部分组成，分别是\npackage main，包声明，go中的文件必须属于某个包，main较为特殊，是程序入口所在； import \u0026ldquo;fmt\u0026rdquo;，导入fmt包，这是一种引入包的方式，接下来就可以使用fmt提供的函数变量； func main() {}，func关键字函数定义，main是函数名，在main包中为程序的入口； fmt.Println，main函数中的代码块，表示调用fmt提供的Println函数打印 字符串\u0026quot;Hello World\u0026quot; 接下来，我们可以使用 go run 执行下这段代码，如下：\n$ go run hello.go Hello World 执行输出 \u0026ldquo;Hello World\u0026rdquo;。\n总结 # 本篇文章从不同系统和不同方式角度出发，介绍了golang在各种场景下的安装方式。之后又详细介绍了几个在go中常用的环境变量，并以一个简单的例子结尾，最终完成了go的安装。\n我的博文：如何安装 Go。\n","date":"2019-04-15","externalUrl":null,"permalink":"/posts/2019-04-15-install-golang/","section":"文章","summary":"本篇文章进入 Go 的开发环境搭建系列。\n我们知道，编写任何语言的代码都离不开两样工具，语言开发包和代码编辑工具。\n今天先来聊聊如何安装 Go。\n我们或许都会觉得这种事非常简单，不值得写篇文章介绍。最初我也是这么想的。但深入了解下来，渐渐感觉这也是一件很有意思的事情。\n","title":"详细聊聊如何安装 Go","type":"posts"},{"content":"新学一门语言，大家都想先弄清楚为什么要学它？玩知乎一段时间更是让我感受深刻，诸如\n为什么要学习Python？ 为什么要学习C？ 为什么要学习Java？ 之类问题经常出现在眼前。以前学语言时倒没怎么关心过这类问题。今年公司由于新业务需要开始全面从PHP转型到Golang。所以我学习它也就是为了工资。额？不能这么俗气，还是具体想想自己为什么要学习Golang吧。\n作为一名golang新人，在写这篇文章时我搜罗到不少golang的优秀资料，在文章最后分享出来。\n大势所趋 # 趋势如此，这应该是多数朋友开始学习它的原因。追涨杀跌，这是大多数人喜欢的操作手法。\n何以证明这个趋势呢？\n首先，我的亲生经历是听到看到golang这个词的频率越来越高，不过，这个太难量化了。来介绍一款工具，google trend，即google趋势。它是google利用自身优势，通过对搜索关键词进行统计分析，根据单词频率分析特定时期某类事物发展趋势的一款分析工具。\n我们可以用 google 趋势来分析一下近年来 golang 的发展趋势，点击链接。\n先看看时间线上的表现，历史的变化趋势：\n可以看出，从2015年到2019年golang的发展趋势一直处在稳定上升阶段；\n不过我们会想，这只能说明golang在世界上整体趋势表现较好，但在中国是否一样火热。这个大可不必担心，google趋势中也有区域的统计信息：\n可以看出，Golang在世界区域的分布情况，前五名分别是，中国、新加坡、圣赫勒拿、韩国、香港。其中，Golang在中国的流行程度简直就是一骑绝尘、遥遥领先。\n注：如果想分析中国各城市的表现情况，可以点击地图就可进入特定国家进行分析。\n除了google趋势，还可以来看看在TIOBE语言排行榜上的表现。点击链接\n额？怎么才十六名，好紧张、好难过，难道学错语言了吗？不对，得找几个理由安慰下自己。\nGolang是一门非常年轻的语言，仅用十年时间就从世界上数以千计的编程语言中脱颖而出，发展速度迅猛。诸如Java、Python、PHP、Javascript都和我一样处在了奔三的路上，近30载的发展才有当前的生态与地位；\nGolang在2018年的最好成绩曾到达过前十名，这个成绩足以说明golang的流行程度。而且排名存在浮动也是很正常的事情，Golang这些年稳步的发展趋势还不能给我们足够的信心吗？\n通过以上的数据分析，我们得到了一些结论，不过感觉说服力不足，有种空喊口号 \u0026ldquo;我们能赢\u0026rdquo; 的感觉。趋势很好，就认为稳赢，显然这是很不合理的。所以，我们还需要分析一些更层次的原因。\n核心成员 # 为什么要了解核心成员呢？核心成员某种意义上是语言的招牌。就像投资，肯定选择相信巴菲特，而不是你。\nGolang的核心开发组成员由一群大神级人物组成。其中，最核心的三人分别是Ken Thompson、Rob Pike、Robert Griesemer。\nRobert Griesemer，参与开发了 Java HotSpot 虚拟机和Javascript的Chrome V8引擎；\nKen Thompson，C和B语言的设计者、Unix创始人之一，操作系统Plan 9的主要作者，1983年图灵奖得主；\nRob Pike，UTF8的主要设计者，与Ken Tompson为贝尔实验室的同事，共同参与了Plan9。而且Golang的logo，据说是囊地鼠，英文gopher，就是Rob Pike的妻子设计的；\n都是如此这般牛人坐镇，可见golang的层次已经高出其他语言很多个台阶了。\n背景历史 # 清楚它的产生背景与发展历史，才能更好了解它的特性与使用场景。\n首先，Golang诞生于google。有了大厂庇护，才好开挂。google曾经一直有个传统，允许员工自由支配本属于工作时间的20%来用于创新实践，这为google带来很多开创性的项目，其中就包括Golang。但听说，前几年该传统已经被取消了。\nGolang早起的讨论由前面介绍的三位大牛发起，针对性分析了当时的环境背景。\n首先，当时传统的编程语言通常都会有如下一些缺点：\n学习成本太高，如C++，为准确表达作者思想，我们要花费大量时间学习语言； 编译速度太慢，代码的编写、预处理、编译与运行流程花费时间太长； 缺乏类型检查，主要指诸如python、php等解释性语言，这常会导致一些低级错误发生； 而且计算机领域相比于前些年也发生了很多变化，比如： 计算机硬件发展迅速，软件已经不能充分发挥它们的优势，比如多CPU； 语言越来越复杂，要么并发与性能不佳，要么风格不够优雅且不统一； 人力成本越高越贵，项目的迭代周期越来越短； 针对如上的各种情况，于是在2007年，他们正式开始着手Golang的设计与开发，并在2009年的11月正式发布。我们列举下，接下来一段时间，Golang发展中几个关键节点。\n2012年3月，正式发布1.0版，走向成熟； 2015年8月，发布了1.5版，实现自编译，移除最后残余的 \u0026ldquo;C代码\u0026rdquo;； 更新迭代速度多，基本保持了每半年更新一个版本；\n2017年2月，发布1.8版 2017年8月，发布1.9版 2018年2月，发布1.10版 2018年8月，发布1.11版 2019年2月，发布1.12版 如此给力的团队与稳定的版本迭代速度，某种程度也促成了golang快速发展。\n语言特性 # 各种介绍go的文章讲的最多的两点特性：静态语言与高并发。但是取交集的话，特性就太少了。介绍细致些吧，但如此一来，这段就显得很是无聊，\n了解golang特性前，可先来看看它的几个设计原则。在网上搜罗了些资料，总结出大概几点：\n大道至简，比如及其简单但完备的面向对象设计，面向接口，没有继承只有组合； 最少特性，一个特性对解决问题有显著效果就没有必要存在； 显式表达，比如数据类型必须显式转化，不提供隐式转化能力； 最少惊异，减少那些奇怪的特性设计，最大程度减少错误发生概率； 从产生背景我们可以知道，Golang在主要针对其他语言痛点而设计的。它有哪些特性？\n静态语言、静态编译速度快，拥有静态语言的安全与性能； 天然支持并发，基于CPS并发模型，goroutine轻量级线程，支持大并发处理； 简洁的脚本化语法，如变量赋值 a := 1，兼具静态语言的特性与动态语言的开发效率； 提供垃圾回收机制，不需要开发人员管理，通过循环判活，再启用goroutine来清理内存； 创新的异常处理机制，普通异常通过返回error对象处理，严重异常由panic、recover处理； 函数多返回值，方便接收多值，一些解释性语言已经支持，如python、js的es6等； 支持defer延迟调用，从而提供了一种方式来方便资源清理，降低资源泄露的概率； 面向接口的oop，没有对象与继承，强调组合，以类似于duck-typing的方式编写面向对象； 那么多特性，好无聊，不对，应该是好厉害。之前在知乎上看到过有位朋友写了个十分钟GO快速入门的文章，挺有趣的，分享出来。看过之后应该对上面这些特性有更直观的认知。\n知乎地址在 GO十分钟快速入门，代码在 GO Play 代码体验。\n优秀项目 # 已经说了那么多Golang的牛x之处，但以前出现过的很多语言也都是这么宣传的。 语言的目标是用于项目开发，并能打造出很多优秀的产品。那么，Golang有哪些好像优秀的项目呢？不搜不知道，一搜吓一跳！列举一下我收集到的golang开发的优秀项目， 如下：\ndocker，golang头号优秀项目，通过虚拟化技术实现的操作系统与应用的隔离，也称为容器； kubernetes，由google开发，简称k8s，k8s和docker是当前容器化技术的重要基础设施； etcd，一种可靠的分布式KV存储系统，有点类似于zookeeper，可用于快速的云配置； codis，由国人开发提供的一套优秀的redis分布式解决方案； tidb，国内PingCAP 团队开发的一个分布式SQL 数据库，国内很多互联网公司在使用； influxdb，时序型DB，着力于高性能查询与存储时序型数据，常用于系统监控与金融领域； cockroachdb，云原生分布式数据库，继NoSQL之后出现的新的概念，称为NewSQL数据库； beego，国人开发的一款及其轻量级、高可伸缩性和高性能的web应用框架； caddy，类比于nginx，一款开源的，支持HTTP/2的 Web 服务端； flynn，一款开源的paas平台； consul，HashiCorp公司推出的开源工具，用于实现分布式系统的服务发现与配置； go-kit，Golang相关的微服务框架，这类框架还有go-micro、typthon； go-ethereum，官方开发的以太坊协议实现； couchbase，是一个非关系型数据库； nsq，一款高性能、高可用消息队列系统，每天能处理数十亿条的消息； packer，一款用来生成不同平台的镜像文件的工具，例如VM、vbox、AWS等； doozer：高速的分布式数据同步服务，类似ZooKeeper； tsuru：开源的PAAS平台，和SAE实现的功能一模一样； gor：一款用Go语言实现的简单的http流量复制工具； 项目列举了这么多，从此也可看出现在很多新项目都在使用Golang开发，涉及到很多领域。\n应用领域 # 接下来了解下Golang具体擅长哪些领域，如果不适合自己所在行业，暂时就没必要去学习了。\n区块链 # 当前的两个主流区块链框架，分布式记账本框架hyperledger和以太坊合约框架go-ethereum都是使用Golang开发；下图是某招聘网站关于区块链职位要求技能的分析。\n微服务 # 现在越来越多的项目会采用微服务架构，前面介绍的优秀项目中也看到很多go提供的微服务框架，如git-kit、micro等。\n举一些具体公司的例子，比如今日头条使用 Golang 构建了千万级微服务；\n云服务 # 云服务，如国内著名的七牛云全站采用Golang开发；还有如盛大CDN、阿里云CDN等；\n很多的云平台基础设施如docker、kubernetes等为Golang开发；\n京东的消息推送与分布式存储也是如此；\n分布式 # 诸如数据库中间件、代理服务等很多采用Golang开发，比如前面的介绍codis、cockroachdb、etcd等；\n其他 # 很多领域都能看到Golang的影子，诸如直播领域、游戏开发等等，在其中golang为后台的调度系统、任务处理，批量的数据计算、系统监控等提供了各种解决方案。 比如，最近知乎近也使用Golang进行重构了自己的推荐系统。\n舍弃 Python，为什么知乎选用 Go 重构推荐系统？\n很多涉及领域就不一一列举了。反正一句话就是很牛。\n学习资料 # 说这么多，主要是为给自己好好学习找个借口。接下来分享一些近期收集的Golang学习资料。\nGolang官网 # Golang官方地址： golang.org，无论学习什么知识，第一手资料基本都是首发于官网。进入到官网后，会看到很多资源，比如：\n文档：golang.org/doc，官方文档，仔细读下文档首页并分类，了解下自己要学哪些内容； 一览：tour.golang.org，交互式运行环境，不安装golang便可体验学习它的语法与使用； 指南：golang.org/ref/spec，golang学习指导手册，从基础语法到高级特性全部都有介绍； 标准库：golang.org/pkg/，可以查看所有的官方库的接口、源码以及使用介绍； 博客：blog.golang.org，不定期分享go的最佳实践，有些公司也会投稿介绍自己的案例； 实验室：play.golang.org，感觉和tour类似，不过在这里编写的代码可以分享给别人； 等等。\n官网是个宝库，我们需要认真仔细去挖掘其中的内容；但由于一些原因，golang的官方站点我们无法访问，不过golang为我们提供了中国的官网，地址：golang.google.cn；\ngolang社区 # 一门语言的发展需要有大批牛人的分享布道，也需要我们这些菜鸟学习有更多的参考路径。这一切都离不开社区。国内外也有很多优秀的go语言社区；\ngo语言中文网，studygolang.com，分享Go语言知识，聚合各种golang文章和书籍资料； go交流论坛，gocn.vip，go语言学习交流论坛； go官方讨论组，forum/golang-nuts，golang的官方邮件讨论组； 一张图、一个目录与一个合集 # 在整理资料时，发现太多优秀的开源项目与书籍，重复工作就不做了，分享几个别人整理的优秀资源。如下：\n一个目录：索引目录地址，各种go语言资源的汇总； 一张图谱：图谱processon地址，源自Golang Foundation； 一个合集：awesome-go，其中整理了大量golang的库、框架和一些著名项目； 我的博文：为什么要学 Go\n","date":"2019-04-08","externalUrl":null,"permalink":"/posts/2019-04-08-why-learn-go/","section":"文章","summary":"新学一门语言，大家都想先弄清楚为什么要学它？玩知乎一段时间更是让我感受深刻，诸如\n为什么要学习Python？ 为什么要学习C？ 为什么要学习Java？ 之类问题经常出现在眼前。以前学语言时倒没怎么关心过这类问题。今年公司由于新业务需要开始全面从PHP转型到Golang。所以我学习它也就是为了工资。额？不能这么俗气，还是具体想想自己为什么要学习Golang吧。\n","title":"为什么要学 Go","type":"posts"},{"content":"","date":"2019-03-18","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"Git","type":"tags"},{"content":"GIT 是当前最流行的版本控制系统。之前在公司系统做过一次系统总结，抽时间整理成文。\n基础概念 # GIT是一种版本控制软件，那就首先了解一下什么是 “版本控制”？\n比较官方的解释是，版本控制系统是一种记录一个或若干个内容变化，以便将来查询特定版本修订情况的系统。\n简言之，你的修改只要提到到版本控制系统，基本都可以找回，版本控制系统就像一台时光机器，可以让你回到任何一个时间点。\n了解了版本控制系统的功能，我们知道就算你把代码改的一塌糊涂，照样可以恢复到我们过去的任何一个时间点，工作量却微乎其微。是不是很amazing。\n总结一下版本控制系统有哪些优点：\n记录文件所有历史变化。这是版本控制系统的基本能力； 随时恢复到任意时间点。历史记录功能使我们不怕改错代码了； 支持多功能并行开发。通常版本控制系统都支持分支，保证了并行开发的可行； 多人协作并行开发。对于多人协作项目，支持多人协作开发的版本管理将事半功倍； 除了上面介绍的几个比较常见的优点，版本控制系统的好处还有很多，就不一一列举了，可通过实践自己逐渐体悟总结。下面基于GIT做更多介绍。\n常见类别 # 版本控制系统常见类别有三种：\n本地版本控制系统、 集中式版本控制系统、 分布式版本控制系统； 本地版本控制系统 # 代表有 RCS(Revision Control System)，Linux下面的可用来作为配置文件管理的版本控制工具，工作使用不多；\n关于其优缺点，简述如下：\n优点：\n简单，很多系统中都有内置； 适合管理文本，如系统配置； 缺点：\n管理少量文件，不支持项目的管理； 支持的文件类型单一； 不支持远程，网络传输； 集中式版本控制系统 # 代表如CVS，SVN(Subversion)，SVN是曾经最流行的版本管理系统，很多人都有用过，因而对于集中式版本控制系统很多人都很了解它。\n优点 # 适合多人团队协作开发； 代码集中化管理； 缺点 # 单点故障； 必须联网，无法单机工作； 优点就不多说了，大家可能对缺点更是记忆深刻。\n单点故障，集中式管理的缺点，代码集中一台机器上，这个问题其实可通过备份集群解决； 必须联网工作，这个缺点深恶痛觉，一旦公司网络出现问题，几个小时甚至一天无法工作； 由于这些缺点，便有了分布版本控制系统。\n分布式版本控制系统 # 代表就是今天要说的GIT了。想知道GIT有多流行吗？看看GITHUB就知道了，现在很多公司都已经把自己的代码库迁移到了GIT。可见GIT在如今的流行程度，也说明分布式版本控制系统是如今的趋势。如下基本架构图：\n关于分布式版本控制系统，这里只说优点：\n适合多人团队协作开发； 代码集中化管理； 可以离线工作； 每个计算机都是一个完整仓库； 前面两点SVN也可以做到，说一下后两点。\n可以离线工作，分布式版本管理系统每个计算机都有一个完整的仓库，可本地提交，可以做到离线工作。没有了SVN令人抓狂的断网无法工作问题;\n每个计算机都是一个完整的仓库，也就没有了SVN的单点故障。\nGIT与SVN的比较 # GIT的作者Linus一直比较痛恨集中方式版本控制系统。虽然有很多已知免费的集中式版本控制系统，但是在2002之前提交Linux源码的方式都是通过diff提交给Linus的，然后进行手工合并。下面让我们来以GIT与SVN作为代表，来看看为什么Linus痛恨集中式版本管理系统，而喜欢分布式版本管理系统。下面具体比较一下它们的区别：\nGIT是分布式的，SVN为集中式的 # 常识，两者最重要的区别，也是后面所有区别的基石；\nGIT 随处都是版本库，SVN 只有一个中央版本库 # 因为GIT是分布式的所以能做到到处都是版本库，而SVN是集中式的，所以只有一个中央仓库。因而GIT能够做到无需网络提交，到处到时版本库，压根不用担心提交速度问题，不用时刻依赖与网络工作，不用担心单点故障。当工作完成之后直接推送远程即可实现工作协作；\nGIT没有全局版本号，SVN有全局版本号 # GIT版本库到处都是，之间没有实时共享数据，所以无法确保版本号的唯一性，无法使用全局版本号，分布在各个机器上的版本库版本号使用40位的HASH值取代。重复的情况是存在的，从数学的角度考虑，可能性是2的63次方分之一，基本可忽略。而对于SVN，唯一版本，所以能够做到使用全局的版本号，版本号采用自增的方式；\nGIT把内容按元数据存储，SVN按文件存储 # GIT存储的不是实际的文件，而是指向性数据。SVN保存的是文件数据。当GIT切换版本的时候，实际上切换的是元数据，而且本地操作，快捷有效；\nGIT记录文件快照，SVN记录文件差异 # GIT的元数据，即指向性数据指向的是实际的文件快照，这也是GIT能够快速切换版本的一个原因。SVN保存的文件数据是各个版本之间的文件差异，所以切换版本的时候需要逐级的差异计算，速度比较慢，而且还需网络传输。当工程较大时，速度与GIT相比差异会相当的大。\nGIT的内容完整性高，SVN完整性低 # 因为GIT的数据记录都有HASH值校验，所以内容完整性较高。而SVN则没有此功能，内容完整性低。；\nGIT架构 # GIT的架构，可以分为几个部分：\n本地工作区(working directory) 暂存区(stage area, 又称为索引区, index)、 本地仓库(local repository)、 远程仓库副本 远程仓库(remote repository)。 如下图：\n上图展示了git的整体架构，以及和各部分相关的主要命令。先说明下其中涉及的各部分。\n工作区(working directory) # 简言之就是你工作的区域。对于git而言，就是的本地工作目录。工作区的内容会包含提交到暂存区和版本库(当前提交点)的内容，同时也包含自己的修改内容。\n暂存区(stage area, 又称为索引区index) # 是git中一个非常重要的概念。是我们把修改提交版本库前的一个过渡阶段。查看GIT自带帮助手册的时候，通常以index来表示暂存区。在工作目录下有一个.git的目录，里面有个index文件，存储着关于暂存区的内容。git add命令将工作区内容添加到暂存区。\n本地仓库(local repository) # 版本控制系统的仓库，存在于本地。当执行git commit命令后，会将暂存区内容提交到仓库之中。在工作区下面有.git的目录，这个目录下的内容不属于工作区，里面便是仓库的数据信息，暂存区相关内容也在其中。\n远程版本库(remote repository) # 与本地仓库概念基本一致，不同之处在于一个存在远程，可用于远程协作，一个却是存在于本地。通过push/pull可实现本地与远程的交互；\n远程仓库副本 # 可以理解为存在于本地的远程仓库缓存。如需更新，可通过git fetch/pull命令获取远程仓库内容。使用fech获取时，并未合并到本地仓库，此时可使用git merge实现远程仓库副本与本地仓库的合并。\nGIT文件一览 # 看看.git这个目录的下文件结构，如下图：\n简要说明一下各个文件中所存放的内容信息：\nHEAD，当前所在位置，其实就是工作区的在版本库中的那个提交点，最终会指向一个40位的HASH值；\nconfig，当前版本库的专有配置文件，如使用命令git config user.name poloxue便会记录在此文件；\ndescription，被gitweb (Github的原型)用来显示对repo的描述。\nhooks，git有可自动运行在 git 任有意义阶段的脚本hooks, 如commit/release/pull/push等状态之前或者之后。个人思考的一个用处，如pre-push可以用来强制进行代码检查。\nindex，存放暂存区(stage area)的相关信息；\ninfo/exclue，可以做到和.gitignore相同的事情，用于排除不要包含进版本库的文件。区别就是，此文件不会被共享。\nrefs/heads，目录下有关于本地仓库的所有分支；\nrefs/remote，目录下有关于远程仓库的所有分支；\nobject，目录下存放的就是实际的数据文件，关于其中的存放方式暂时还不了解，有兴趣可以研究一下；\n最后 # 本节从版本控制引出分布式版本控制，比较分布式版本控制系统与其他版本控制系统的区别，从而引入GIT。与SVN的比较明显可看出GIT的优秀之处。对GIT的架构也进行整体简单介绍。内容比较理论，期望在理清自己思路的同时，不会误导他人。\n","date":"2019-03-18","externalUrl":null,"permalink":"/posts/2019-03-18-git-architecture/","section":"文章","summary":"GIT 是当前最流行的版本控制系统。之前在公司系统做过一次系统总结，抽时间整理成文。\n基础概念 # GIT是一种版本控制软件，那就首先了解一下什么是 “版本控制”？\n","title":"谈谈版本管理 GIT 的理论与架构","type":"posts"},{"content":"","date":"2016-12-31","externalUrl":null,"permalink":"/tags/crontab/","section":"Tags","summary":"","title":"Crontab","type":"tags"},{"content":"此篇技术博文主要介绍的是 crontab， Linux 下的计划任务管理工具。涉及内容包括 crontab 使用配置、常见坑的分析和编者总结的错误调试方法。\n我的理解，后台任务通常分为两种：常驻和定时。之前的文章《pm2进程管理工具使用总结》主要针对的是常驻任务。今天来谈谈 crontab，主要针对的是定时任务。\n实验环境： centos7\n介绍crontab # crontab的服务进程名为crond，英文意为周期任务。顾名思义，crontab在Linux主要用于周期定时任务管理。通常安装操作系统后，默认已启动crond服务。crontab可理解为cron_table，表示cron的任务列表。类似crontab的工具还有at和anacrontab，但具体使用场景不同，可参见附录《让你学会Linux计划任务》一文了解更多。\n关于crontab的用途很多，如\n定时系统检测； 定时数据采集； 定时日志备份； 定时更新数据缓存； 定时生成报表； \u0026hellip; 等等任务 当然，更多使用场景是要以视具体情况而定了。毕竟是工具通常都是常用规则总结而成的产物。\n确认crond服务已经安装与开启之后，下面开始具体说明\n简单示例 # 先来个简单示例体验一下。\n目标：每分钟向/tmp/time.txt文件下写入当前时间 新建crontab任务 $ crontab -e // 打开crontab任务编辑 * * * * * date \u0026gt;\u0026gt; /tmp/time.txt 静静等待几分钟 $ cat /tmp/time.txt Do 29. Dez 22:45:01 CST 2016 Do 29. Dez 22:46:01 CST 2016 Do 29. Dez 22:47:01 CST 2016 从上面结果看出，每分钟执行了date并写入到/tmp/time.txt。 简单示例演示成功。下面从细节深入说明crontab使用。\n使用选项 # 上面的实验中使用了crontab命令的-e选项。我们来看看crontab命令中有哪些选项?\n-e 选项 表示打开当前用户的crontab任务列表配置文件。当然也可以直接打开，路径通常是在/var/spool/cron/下，文件以用户名命名，如/var/spool/cron/root。不过，采用-e方式打开，福利是可以帮助我们自动检查任务配置符合规则。\n-u 选项 指定某用户的任务列表，很好理解。比如我当前是root用户，想操作poloxue用户的任务列表。如下：\n$ crontab -u poloxue -e -l 选项 列出某用户的所有任务列表 -r 选项 删除某用户的所有任务列表，这个选项使用小心为上，估计也只是自己实验时玩玩而已，正常不使用。\ncrontab命令的选项中，主要使用的就是以上几个，理解比较简单。\n任务配置 # 说完了crontab的命令选项，下面开始真正的大戏，任务列表文件如何配置？\n首先，看下crontab任务列表配置格式，示例文件如下：\nSHELL=/bin/bash PATH=/sbin:/bin:/usr/sbin:/usr/bin MAILTO=root # 更多细节 man 4 crontabs # 计划任务定义的例子: # .---------------- 分 (0 - 59) # | .------------- 时 (0 - 23) # | | .---------- 日 (1 - 31) # | | | .------- 月 (1 - 12) # | | | | .---- 星期 (0 - 7) (星期日可为0或7) # | | | | | # * * * * * 执行的命令 * * * * * date \u0026gt;\u0026gt; /time.txt 2\u0026gt;\u0026amp;1 从上面的示例文件可看出，crontab的任务列表主要由两部分组成：环境变量配置与定时任务配置。可能大家在工作中更多是只用到了任务配置部分。\n环境变量配置部分 # 理解环境变量配置这部分可以帮助我们减少去踩一些不必要的坑。简单说明上面涉及的环境变量。\nSHELL为/bin/bash，表示使用/bin/bash解释执行命令\nPATH表示到哪些目录路径寻找命令程序，此环境变量的值说明了为什么我们在crontab中执行命令时，尽量要写命令全路径才能执行的原因。\nMAILTO变量作用是当任务执行有输出时，内容发送到哪个用户的邮箱。禁用可以设置MAILTO=\u0026quot;\u0026quot;。\n当我们在使用crontab时，发现某些定时任务不能顺利执行，但shell控制台执行成功，环境变量是否正确是我们需要首先关注的点之一。具体详情可以看后面关于环境变量坑的说明。\n定时任务配置部分 # 这部分是crontab配置核心。\n基本配置 # 如下所示配置共6列，前5列是关于执行时间配置，最后1列是具体执行命令。\n.---------------- 分 (0 - 59) | .------------- 时 (0 - 23) | | .---------- 日 (1 - 31) | | | .------- 月 (1 - 12) | | | | .---- 星期 (0 - 6) (星期日可为0或7) | | | | | * * * * * 执行的命令 第一列单位为分，表示每时第几分钟，范围为0-59； 第二列单位为时，表示每天第几小时，范围为0-23； 第三列单位为日，表示每月第几天，范围为1-31； 第四列单位为月，表示每年第几月，范围为1-12； 第五列单位为星期，表示每星期第几天，范围0-7，0与7表示星期日，其他分别为星期1-6；\n时间配置段类型 # 根据时间列中值的不同设置方式，编者总结出以下五种类型：\n固定某值，指定固定值，如指定1月1日0时0分执行任务\n0 0 1 1 * command 月日时分都指定了固定数值。 注：*在crontab中表示任意值都满足条件。\n列表值，时间值是一个列表，如指定一个月内2、12、22日零时执行任务\n0 0 2,12,22 * * command 上述日指定多个值，2号、12号和22号，以逗号分隔；\n连续范围值，时间为连续范围的值，如指定每个月1至7号零时执行任务\n0 0 1-7 * * command 上述日期为连续范围的值1-7时\n步长值，根据指定数值跳跃步长确定执行时间，如指定凌晨1时开始每割3个小时0分执行一次任务\n0 1-24/3 * * * command 上述指定从凌晨1时每3个小时执行任务，如1点0分，4点0分，7点0分等。\n混合值，支持以上类型的组合，如指定每小时0至10分，22、33分以及0-60分钟每隔20分钟执行任务，如下\n0-10,22,33,*/20 * * * * command 这里的分钟值采取了多种类型组合指定，包括连续范围值(0-7)，列表值(22,33)，步长值(*/20)。\n声明：这几种时间配置类型是编者自己总结，希望能帮助大家更好理解。有错误帮忙指出。\n定时语句解析工具 # 通常在使用crontab添加任务时，我们会依靠自己已有知识编写定时语句。当需要测试语句是否正确时，总需要一定时间等待证明其正确性。作为一名牛逼的程序员，这种方式就太不酷了。有没有一款工具，只要我们给出语句，其就能告诉具体执行时间呢？下面介绍一款老外开发的crontab在线解析工具。\n工具地址：https://crontab.guru\n下面是这个工具的截图\n从上面看出，我们输入的语句解析结果为每天的04：05执行任务。下面有这样一行文字“next at 2016-12-31 04:05:00”，告诉了我们最近一次的执行时间。\n注明：百度搜索“crontab在线解析”获得的工具有坑，某些语句解析结果错误。为避免大家受骗，这里提供具体地址：http://tool.lu/crontab/\n使用有坑 # crontab使用中常会遇到各种坑。下面列出编者在使用中曾遇到的一些问题。\n时间配置误区 # 此处介绍两种坑，一种是由于基本功不足导致配置错误，而另一种则是多数人对crontab配置都存在的一个理解误区。\n整点时间设置错误 # 其实这个错误不用单独说明，但是编者刚开始接触crontab时犯过，单独拿出来说明一下。\n如设定每天3点执行一次某任务\n下面列出错误方式，当我们听到每天3点执行一次某任务时，很多人会把重点放在3点，而忽略了执行一次的需求。\n下面是个错误的例子\n* 3 * * * command 这里会导致在三点的每分钟都会执行一次任务，也就是执行了60次。 正确方式如下，每天3点0时执行任务\n0 3 * * * command 日与星期的关系误区 # 这真的是个大误区，很多人都不知道的大误区。直接开始说明吧。\n好，首先做两个练习\n设置任务一：每月的1-7每天零时执行某任务，答案如下：\n0 0 1-7 * * date \u0026gt;\u0026gt; /tmp/date.txt 设置任务二：每星期的星期一零时执行某任务，答案如下：\n0 0 * * 1 date \u0026gt;\u0026gt; /tmp/date.txt 上面两个任务的设定都是正确的。\n下面提出第三个任务，设置每个月的第一个星期一零时执行某任务\n分解任务要求，首先，第一个星期就是每个月的1-7日，而星期一就是星期一。所以我们理解的crontab任务配置如下\n0 0 1-7 * 1 date \u0026gt;\u0026gt; /tmp/date.txt 下面直接使用前面介绍的在线解析工具分析此语句，如下\n解析结果显示语句执行时间为每月的1至7日和每星期一。可以看到最近执行时间是“next at 2017-01-01 00:00:00”，这个时间也并非星期一。\n这是crontab的一个特别容易误解之处，下面直接给出结论:\n当日和星期任一列包含*时，日与星期两者为并且的关系； 当日和星期列中不包含*时，日与星期两者为或者的关系； 请注意，前面提到的那个百度搜索出来的工具分析结果显示的确是每月第一个星期一，这是错误的。如有朋友持怀疑态度，可自行验证，如有错误，随时告知。\n环境变量问题 # 当我们刚使用crontab时，有人会告知所有命令尽量都使用绝对路径，以防错误。为什么？这就和我们下面要谈的环境变量有关了。\n首先，获取控制台环境变量看下\n$ env XDG_SESSION_ID=10 HOSTNAME=localhost.localdomain SHELL=/bin/bash PERL_MB_OPT=--install_base /root/perl5 USER=root MAIL=/var/spool/mail/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/php5/bin PWD=/var/mail SHLVL=1 HOME=/root LOGNAME=root XDG_RUNTIME_DIR=/run/user/0 _=/usr/bin/env 考虑篇幅，输出有删减。\n然后，获取crontab环境变量信息\n* * * * * /usr/bin/env \u0026gt; /tmp/env.txt 输出结果，如下\n$ cat /tmp/env.txt XDG_SESSION_ID=732 SHELL=/bin/sh USER=root PATH=/usr/bin:/bin PWD=/root LANG=de_DE.UTF-8 SHLVL=1 HOME=/root LOGNAME=root XDG_RUNTIME_DIR=/run/user/0 _=/usr/bin/en 对比分析两者输出\n对比crontab与控制台输出，我们发现两者的环境变量差异很大。如果命令在控制台执行成功，而在crontab执行失败，我们需要考虑是否命令涉及的环境变量在crontab和控制台间存在差异。\n明白crontab使用绝对路径执行命令原因了吗？\n我们知道命令默认查找路径是由PATH指定的。\n从上面输出结果可知，控制台的PATH值为\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/php/bin crontab的PATH值为\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/php/bin crontab的PATH值为\nPATH=/usr/bin:/bin /usr/local/php/bin/下面存在php命令，在控制台执行成功\n$ php index.php 因在crontab的PATH变量无/usr/local/php/bin/，其执行php命令则会失败。\n解决方式 # 已知哪个环境变量导致问题，可以直接在crontab配置中加入变量配置。\n不知哪个环境变量导致问题，终极大招是引入控制台环境变量，如下\n* * * * * source /$HOME/.bash_profile \u0026amp;\u0026amp; command 当然，对于某特定环境变量或有特定的处理方式，如PATH，命令使用绝对路径亦可解决。\n特殊符号% # %在crontab是特殊符号，具体含义如下：\n第一个%表示标准输入的开始\n* * * * * cat \u0026gt;\u0026gt; /tmp/cat.txt 2\u0026gt;\u0026amp;1 % stdin input 执行成功之后，查看/tmp/cat.txt\n$ cat /tmp/cat.txt stdin input 我们看到标准输入写入到了/tmp/cat.txt文件。\n理解上面示例，首先需知cat \u0026raquo; /tmp/cat.txt ，作用是将标准输入重定向至/tmp/cat.txt。\n其余%表示换行符\n示例如下\n* * * * * cat \u0026gt;\u0026gt; /tmp/cat_line.txt 2\u0026gt;\u0026amp;1 % stdin input 1 % stdin input 2 % stdin input 3 查看输出\n$ cat /tmp/cat_line.txt stdin input 1 stdin input 2 stdin input 3 有三行输出\n解决方式\n既然是特殊字符，自然而然就想到了使用\\进行转义，如下：\n* * * * * cat \u0026gt;\u0026gt; /tmp/cat_special.txt 2\u0026gt;\u0026amp;1 % per cent is \\%. 2\u0026gt;\u0026amp;1 查看输出\n$ cat /tmp/cat_special.txt per cent is %. 执行成功了。自此，你就顺利爬出了%特殊字符问题的坑。\n关于这个问题的具体说明，可以参看附录中的《Crontab and %》。\n关于输出重定向 # 当我们不做输出重定向时，如任务有大量输出，或许有些无法解释的问题。\n输出写入邮件\ncrontab任务输出默认写入到执行用户的邮件中，如下演示：\n* * * * * date 命令输出当前日期，下面查看当前用户的邮件\n$ cat /var/spool/mail/$USER ... Sat Dec 31 17:45:01 CST 2016 由此可见，任务输出的日期信息写入到了用户邮件中。\n如任务有大量输出，会占用磁盘资源。但编者测试显示，如磁盘容量不足，任务也会执行，但输出不会写入邮件；\n关闭邮件功能\n如何关闭？设置MAILTO环境变量为空。如下\nMAILTO=\u0026#34;\u0026#34; * * * * * date 是不是关闭邮件写入就好了？附录《Linux中的crontab与sendmail》博文表明，关闭mail功能，输出内容将写入到/var/spool/clientmqueue中，可能占满分区的inode资源，导致任务无法执行。inode资源使用情况可通过如下命令获取\n$ df -i Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sda1 512000 378 511622 1% /boot /dev/sda2 92672000 185351 92486649 1% / 抱歉！这种情况编者并未测出！但在公司的生产环境发现过未重定向则任务不执行的情况，加上后解决了问题。百度也搜索到了类似问题，如有朋友了解，欢迎指教，万分感谢。\n当然，为了避免此类问题发生，建议任务都加上输出重定向，如下\n* * * * * date \u0026gt;\u0026gt; /dev/null/ 2\u0026gt;\u0026amp;1 输出到/dev/null中，标准输入和标准错误都应处理。\n如大家对重定向有疑惑，可参见附录中的《Linux重定向》，对文解释不错。\n程序员的感悟：在技术的世界，当我们不按常理做事，事情也不会按常理犯错。\n调试大招 # 最后的福利，编者根据自己的总结而梳理出一套快速定位crontab错误的思路。两个角度：\n任务是否执行 命令是否正确 任务是否执行？ # 调试思路\n首先，通过日志确认任务是否执行 然后，如未执行则分析定时语句， 最后，定时没有问题，检查crond服务是否开启\n下面说明具体分析步骤。\n日志确认\n调试错误，日志通常是个利器，crontab也有日志。 编者的服务器中crontab日志文件位置为/var/log/cron\n查看日志 日志中包含任务执行记录，配置错误提示，任务配置编辑重载记录，服务开启等记录。\n下面是日志的部分内容，\n$ vim /var/log/cron ... Dec 31 19:17:01 localhost crond[1455]: (CRON) bad day-of-week (/var/spool/cron/root) Dec 31 19:17:01 localhost CROND[4409]: (root) CMD (date) ... 这里截取了对调试比较重要的两条记录，如下介绍\n执行记录\nDec 31 19:17:01 localhost CROND[4409]: (root) CMD (date) 显示12月21 19时17分1秒执行了date命令\n配置错误\nDec 31 19:17:01 localhost crond[1455]: (CRON) bad day-of-week (/var/spool/cron/root) 上面显示/var/spool/cron/root的任务配置有错，也就是root任务配置有错。错误原因：bad day-of-week，星期配置有错。\n语句是这样的\n* * * * date \u0026gt;\u0026gt; /dev/null 2\u0026gt;\u0026amp;1 明显缺少了星期时间段。\n确认定时语句\n通过上面的日志分析，如任务没有执行，使用定时语句在线分析工具分析定时是否正确，非常简单。\n确认服务开启 如果定时语句也正确，检查服务是否开启。检测命令如下\nSystemd方式(centos7及以上)\n$ systemctl status crond.service SysVinit方式(centos7以下)\n$ service crond status 查看命令输出，如未开启，执行如下命令开启\nSystemd方式(centos7及以上)\n$ systemctl start crond.service SysVinit方式(centos7以下)\n$ service crond start 确认任务成功后，如问题仍未解决，继续往下看。\n命令是否正确 # 确认命令成功与否，这里总结步骤大致如下\n获取命令执行输出\ncrontab中的命令执行出错，多数人都不知道如何调试。我们知道在控制台执行命令时，可通过输出获取错误信息调试问题。这种方式在crontab同样适用，方法就是利用重新向获取输出，进行分析。示例如下\n* * * * * php /root/index.php \u0026gt;\u0026gt; /tmp/debug.log 2\u0026gt;\u0026amp;1 这条任务总是执行失败，我们把输出重定向到/tmp/debug.log。\n查看debug.log，如下\n$ cat /tmp/debug.log /bin/sh: php: command not found /bin/sh: php: command not found 显示php命令没有找到，很明显的就可以确定是环境变量的问题。这种方式定位问题非常有效。\n具体问题具体分析\n有了命令执行的输出，下面就是具体问题具体分析了。或许是前面提到的各种坑，也或许是命令本身所独有的问题。\n调试的方法到这里就说完了。但还是实践为王，需持续总结，同时也希望大家不要在同样的坑中重复犯错。\ncrontab写了这么长，希望能切实帮到大家。有哪位朋友看到了最后吗？表示佩服！\n参考附录 # 让你学会Linux计划任务 Linux中的crontab与sendmail Crontab and % Linux重定向 ","date":"2016-12-31","externalUrl":null,"permalink":"/posts/2016-12-31-crontab-from-scratch/","section":"文章","summary":"此篇技术博文主要介绍的是 crontab， Linux 下的计划任务管理工具。涉及内容包括 crontab 使用配置、常见坑的分析和编者总结的错误调试方法。\n我的理解，后台任务通常分为两种：常驻和定时。之前的文章《pm2进程管理工具使用总结》主要针对的是常驻任务。今天来谈谈 crontab，主要针对的是定时任务。\n","title":"一文精通 crontab 从入门到出坑","type":"posts"},{"content":"","date":"2016-12-10","externalUrl":null,"permalink":"/tags/mysql/","section":"Tags","summary":"","title":"Mysql","type":"tags"},{"content":"本篇文章的重点在于总结MYSQL事务。\n什么是事务 # 事务简言之就是一组 SQL 执行要么全部成功，要么全部失败。MYSQL 的事务在存储引擎层实现。\n事务都有 ACID 特性：\n原子性（Atomicity）：一个事务必须被视为一个不可分割的单元； 一致性（Consistency）：数据库总是从一种状态切换到另一种状态； 隔离性（Isolation）：通常来说，事务在提交前对于其他事务不可见； 持久性（Durablity）：一旦事务提交，所做修改永久保存数据库； 事务最常用的例子就是银行转账。假设 polo 需给 lynn 转账1000元，如下步骤：\n确认 polo 账户余额高于1000元； 从 polo 的账户余额减去1000元； 将 lynn 的账户余额增加1000元； SQL语句如下:\nmysql\u0026gt; BEGIN; mysql\u0026gt; SELECT balance FROM bank_account WHERE uid=10001; mysql\u0026gt; UPDATE bank_account SET balance=balance-1000 WHERE uid=10001; mysql\u0026gt; UPDATE bank_account SET balance=balance+1000 WHERE uid=10002; mysql\u0026gt; COMMIT; mysql 启动事务可使用 BEGIN 或 START TRANSACTION；上述三个步骤执行在一个事务中就能够保证数据的完整性，要么全部成功，要么全部失败。\nMYSQL 提供两种事务型引擎：Innodb 和 NDBCluster。默认采用自动提交模式，执行一条语句自动 COMMIT。通过 AUTOCOMMIT 变量可启用或者禁用自动提交模式：\nmysql\u0026gt; SHOW VARIABLES LIKE \u0026#34;AUTOCOMMIT\u0026#34;; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec) mysql\u0026gt; SET AUTOCOMMIT=1 AUTOCOMMIT=1 表示开启默认提交，0表示关闭默认提交需要手动提交。\n隔离级别 # 事务隔离性的解释：通常情况下，事务在提交之前对于其他事务不可见。\n数据库有四种隔离级别，当然 MYSQL 也是如此。分别是：\n未提交读，READ UNCOMMITTED 已提交读，READ COMMITTED 可重复读，REPEATABLE READ 串行化，SEAIALIZABLE 关于隔离级别的两个理解\n书本解释，每种级别都规定了一个事务中所做修改，哪些在事务内和事务间是可见的。 我的理解，隔离级别就是决定一个事务的修改另一个事务什么情况下能看到。 两者区别在于是否存在事务内可见性，但无论哪个级别在事务内的操作肯定是可见的，重点在事务间可见性。\n下面开始说明 MYSQL 的四种隔离级别，先准备一张学生表：\nmysql\u0026gt; CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL DEFAULT \u0026#39;\u0026#39;, PRIMARY KEY (`id`) ) DEFAULT CHARSET=utf8 | 只有id（主键自增）与name字段\n未提交读 # 事务中修改没有提交对其他事务也是可见的，俗称脏读。非常不建议使用。\n示例演示,客户端A和B设置隔离级别为未提交读\nmysql\u0026gt; SET SESSION TX_ISOLATION=\u0026#39;READ-UNCOMMITTED\u0026#39;; 客户端A与B开启事务并查询student\nmysql\u0026gt; BEGIN； mysql\u0026gt; SELECT * FROM student; Empty set (0.00 sec) 当前，客户端 A 和 B 都是空数据。此时在客服端 B 插入一条新的数据\nmysql\u0026gt; INSERT INTO student(name) VALUES(\u0026#34;polo\u0026#34;); Query OK, 1 row affected (0.00 sec) 此时事务还未提交，客服端 A 查看一下 student 表，如下：\nmysql\u0026gt; SELECT * FROM student; +----+------+ | id | name | +----+------+ | 1 | polo | +----+------+ 1 row in set (0.00 sec) 可以看出，客户端 A 看到 B 未提交的修改。客户端 B 执行回滚操作，如下：\nmysql\u0026gt; ROLLBACK 成功之后，客户端 A 查看 student 表：\nmysql\u0026gt; SELECT * FROM student; Empty set (0.00 sec) 输出显示，客户端A查看数据为空。\n以上可以看出未提交读隔离级别的危险性，对于一个没有提交事务所做修改对另一个事务是可见状态，容易造成脏读。非特殊情况不得使用此级别\n读提交读 # 多数数据库系统默认为此级别（MYSQL不是）。已提交读级别即为一个事务只能看到已提交事务所做的修改，也就解决了未提交读的问题，即脏读的问题。\n示例演示，客户端A和B设置隔离级别为已提交读，执行如下命令：\nmysql\u0026gt; SET SESSION TX_ISOLATION=\u0026#39;READ-COMMITTED\u0026#39;; 客户端 A与 B 开启事务并查询 student\nmysql\u0026gt; BEGIN; mysql\u0026gt; SELECT * FROM student; Empty set (0.00 sec) 结果显示，客户端A和B都为空。接着，客户端 B 插入一条新的数据但不执行提交：\nmysql\u0026gt; INSERT INTO student (name) VALUES(\u0026#39;polo\u0026#39;); 接下来，客户端 A 查看一下 student 数据：\nmysql\u0026gt; SELECT * FROM student; Empty set (0.00 sec) 注意这里与上面不同了，在客户端B没有提交事务情况下无数据。下面客户端B提交事务：\nmysql\u0026gt; COMMIT; 客户端 A 再查看下 student 表。\nmysql\u0026gt; SELECT * FROM student; +----+------+ | id | name | +----+------+ | 1 | polo | +----+------+ 1 row in set (0.00 sec) 这样我们就成功读取到了客户。\n从上面的示例可以看出，提交读没有了脏读问题，但我们可以看到在客户端 A 的一个事务中执行两次同样的 SELECT 语句得到不同结果，因此已提交读又被称为不可重复读。同样筛选条件可能得到不同的结果。\n可重复读 # 如其名所言，解决已提交读不可重复读取的问题。\n示例演示，客户端A和B设置隔离级别为可重复读。首先设置隔离级别：\nmysql\u0026gt; SET SESSION tx_isolation=\u0026#39;REPEATABLE-READ\u0026#39; 客户端A与B开启事务并查看\nmysql\u0026gt; BEGIN; mysql\u0026gt; SELECT * FROM student; +----+------+ | id | name | +----+------+ | 1 | polo | +----+------+ 1 rows in set (0.00 sec) 客服端 B 更新 polo 为 adam 并提交事务\nmysql\u0026gt; UPDATE student SET name=\u0026#39;adam\u0026#39; WHERE id=1; mysql\u0026gt; COMMIT 客户端A查看student表，结果如下：\nmysql\u0026gt; SELECT * FROM student; +----+------+ | id | name | +----+------+ | 1 | polo | +----+------+ 1 rows in set (0.00 sec) 客户端 A 查看数据未变，没有不可重复读问题。\n客户端 A 提交事务，并查看student表。\nmysql\u0026gt; COMMIT; mysql\u0026gt; SELECT * FROM student; +----+------+ | id | name | +----+------+ | 1 | polo | +----+------+ 1 rows in set (0.00 sec) 从上面的示例可知，可重复读两次读取内容一样。该级别并没有解决幻读的问题。但是MYSQL在可重复读基础上增加了MVCC机制解决了此问题，此处无法演示幻读的效果。\n那什么是幻读？首先，可重复读锁定范围为当前查询到的内容，如执行\nmysql\u0026gt; SELECT * FROM student WHERE id\u0026gt;=1 锁定的即 id\u0026gt;=1 查到的行，为行级锁。如另一事务执行并默认提交以下语句\nmysql\u0026gt; INSERT INTO student (name) VALUES (\u0026#39;stephen\u0026#39;); 新增的这行并没有被锁定，此时读取 student\nmysql\u0026gt; SELECT * FROM student WHERE id\u0026gt;=1; +----+---------+ | id | name | +----+---------+ | 1 | polo | | 2 | stephen | +----+---------+ 2 rows in set (0.00 sec) 出现了幻读。关于这个问题，除了使用 MYSQL 的 MVCC 机制，还可以用可串行化隔离级别解决此问题。\n串行化 # 串行化是最高隔离级别，强制事务串行执行。执行串行了，那么也就解决了一切的问题，这个级别只有在对数据一致性要求非常严格且没用并发的情况下使用。\n示例演示，客户端 A 和 B 设置隔离级别为可串行化。\nmysql\u0026gt; SET SESSION tx_isolation=\u0026#39;SERIALIZABLE\u0026#39;; 首先，客户端 A 执行一下查询：\nmysql\u0026gt; SELECT * FROM student WHERE id\u0026lt;4; +----+---------+ | id | name | +----+---------+ | 1 | polo | | 2 | stephen | +----+---------+ 2 rows in set (0.00 sec) 接下来，客户端 B 执行数据新增：\nmysql\u0026gt; INSERT INTO student (name) VALUES(\u0026lsquo;yunteng\u0026rsquo;); 好的！效果出现了，此时我们会发现INSERT语句被阻塞执行，原因就是A执行了查询表student同时满足id\u0026lt;4，已被锁定。如果查询表student条件为id\u0026lt;3，则新增语句可正常执行。\n汇总图 # 隔离级别 英文 脏读 不可重复读 幻读 加锁读 未提交读 READ UNCOMMITTED 是 是 是 否 提交读 READ COMMITTED 否 是 是 否 可重复读 REPEATABLE READ 否 否 是 否 串行化 SERIALIZABLE 否 否 否 是 关于事务的隔离级别到此结束。具体使用何种情况还要根据具体的业务场景来决定。\n","date":"2016-12-10","externalUrl":null,"permalink":"/posts/2016-12-10-isolation-in-mysql/","section":"文章","summary":"本篇文章的重点在于总结MYSQL事务。\n什么是事务 # 事务简言之就是一组 SQL 执行要么全部成功，要么全部失败。MYSQL 的事务在存储引擎层实现。\n","title":"循序渐进 MySQL 事务隔离级别","type":"posts"},{"content":"","date":"2016-03-30","externalUrl":null,"permalink":"/tags/kafka/","section":"Tags","summary":"","title":"Kafka","type":"tags"},{"content":"","date":"2016-03-30","externalUrl":null,"permalink":"/tags/mq/","section":"Tags","summary":"","title":"Mq","type":"tags"},{"content":"今天来聊下大数据场景下比较流行的消息队列组件 kafka。本篇文章将主要从理论角度来介绍。\nkafka 是一款开源、追求高吞吐、实时性，可持久化的流式消息队列，可同时处理在线（消息）与离线应用(业务数据和日志)。在如今火热的大数据时代，得到了广泛的应用。\n整体架构 # kafka 的消息以 Topic 进行归类，支持分布式 distribution、可分区partition 和可复制 replicated 的特性。下面为本人梳理的一张 Kafka 系统架构图。\nKafka的架构相较于其他消息系统而言，比较简单。其整体流程简述如下\nProducer 与指定 Topic 各分区 Partition 的 Leader 连接，从而将消息 push 到 Broker 中。\nBroker 可理解消息系统的中间代理，将消息写入磁盘实现持久化，并可对消息复制备份。\nConsumer 采用 pull 的方式主动获取 broker 中指定 Topic 的消息，并进行处理。\nZookeeper负责Kafka服务相关metadata的存储，如broker，topic和consumer等信息的存储。\n注：zookeeper是一个分布式协调服务，分布式应用可基于它实现同步服务，配置维护和命名服务等。此篇文章不做介绍，以后有时间再做总结！\n下面对涉及的各个组件作详细介绍。\n主题Topic # 首先，Kafka中的消息以Topic分类管理。在Kafka中，一个topic可被多个Consumer订阅。通过集群管理，每个Topic可由多个Partition组成。如下图\n从上图可以看出，Topic中数据是顺序不可变序列，采用log追加方式写入，因而kafka中无因随机写入导致性能低下的问题。\nTopic的数据可存储在多个partition中，即可存放在不同的服务器上。这可使Topic大小不限于一台server容量。同时，消息存在多个partition上，可以实现Topic上消息的并发访问。\nKafka中数据不会因被consumer消费后而丢失，而是通过配置指定消息保存时长。Topic中每个partition中的消息都有一个唯一的标识，也称为offset。因数据不会因消费而丢失，所以只要consumer指定offset，一个消息可被不同的consumer多次消费。\n基于此，消息获取即可采用顺序访问，我们也可以指定任意offset随机访问，且不会对其他consumer产生影响。\n分布式Distribution # Kafka 的集群分布式主要涉及两个内容：Partition 分区与 Replication 备份。\nPartition 实现将 Topic 中的各个消息存储到不同的分区中，从而分布在不同的 Kafka 节点之上，使 Topic 的数据大小不限于一台 Server。\nReplication 主要用于容错，对一个 Partition 复制多份，存储在不同 kafka 节点上。这可防止因某一分区数据丢失而导致错误。\n虽然 Relication 复制 Partition 多份，但其中只有一个为 Leader 角色，其余 Partition 角色皆为 Follower。Producer 发布消息都是由Leader 负责写入，并同步到其他的 Follower 分区中。如果 Leader 失效，则某个 Follower 会自动替换，成为新的Leader分区。此时，Follower 可能落后于 Leader，所以从所有 Follower 中选择一个”up-to-date”的分区。\n关于性能方面，考虑 Leader 不但承载了客户的连接与消息写入，还负责将消息同步至不同的 Follower 分区上，性能开销较大。因此，不同 Partition 的 Leader 分布在不同的 kafka 节点上，从而防止某个节点压力过载。\n为了更好了解 Partition 与 Replication 关系。举个例子，假设现有一个 Topic 名为 spark_topic，其 Partition 分区数量为 3，Replication 备份因子为 2。则效果如下图\nspark_topic存在spark_topic-1，spark_topic-2，spark_topic-3共三个分区。而每个分区均有两处备份，如spark_topic-1，其同时存在于kafka节点broker0与broker1上，其中broker01上的分区角色为Leader。\n消费者Consumer # Consumer 负责消费消息。Kafka 中 Consumer 消费消息采用 fetch 方式主动拉取，这种方式的好处是 Consumer 客户端能根据自己的处理消息能力决定消息获取的速度与批量获取的数量，从而防止系统过载。\nKafka 的消息并不会因为消息被 Consumer 消费而丢失，因而其提供一个唯一的标识 offset 实现消息的顺序获取，而 offset 需要 consumer 自行维护，非 kafka 节点服务管理。这不同于传统的消息系统。在 Kafka 集群中，消费者的信息与 offset 在 zookeeper 也有保存维护，Consumer 会间歇性向 zookeeper 同步 offset。\nKafka 的 Consumer 提供分组功能，每个 Consumer 都属于一个分组。那分组的作用是什么呢？\n类似queue模式，一个Consumer分组的多个Consumer订阅同一个Topic，一条消息只分发给其中一个Consumer，实现负载均衡效果。\n发布订阅模式，而不同组的多个Consumer订阅同一个Topic，一条消息会广播给在不同分组的所有Consumer。\n请注意，在 Kafka 中，同一 Consumer 分组中，一个 Consumer 只能订阅一个 Topic 中的 Partition，因而在一个 Consumer 分组中，同时订阅同一个 Topic 的 Consumer 的个数不能超过 Partition 分区数。可参看上图所示。\n同样，为减少网络 IO 开销，Consumer 可采用 batch fetch 方式实现一次批量获取多条消息。\n应用场景 # 下面是一些官网介绍的 Kafka 应用场景，包括消息系统、网站行为跟踪、应用监控、日志收集等等。\n消息系统 # Kafka 可以作为传统消息系统的替代。相比传统消息，Kafka 有更高的吞吐量、拥有内置的分区 Partition、复制备份高容错能力。\n传统消息系统对高吞吐量没有过高要求，但 kafka 的低延迟特性和强大的备份容错能力是传统消息所必须的。\n网站行为追踪 # Kafka 可用于用户行为追踪，通过将用户行为数据发送给 Kafka。以此为基础，实现用户行为在线与离线分析，可用于网站实时监控与异常行为拦截等。\n日志收集 # Kafka 可以作为日志收集解决方案。日志收集通常是将不同服务器的日志文件收集到一个中心区域，Kafka 实现了对日志文件数据进行抽象，统一了处理接口。Kafka 低延迟，支持不同的日志数据源，分布式消费易于扩展，可同时将数据提供给hdfs、storm、监控软件等等。\n应用监控 # Kafka可用于监控运行中的应用系统。如收集分布式应用的数据进行聚合计算，进行分析检测异常情况。\n个人感觉，本质和网站行为分析异常监控有异曲同工之处，只不过所监控的数据对象不同罢了。\n结束 # 本文总结了大数据中常用的消息队列服务 Kafka。本篇主要从架构角度介绍。个人感觉，介绍系统架构比操作实战更加困难，文章如有错误，请帮忙请指正。\n博文地址：快速了解 Kafka 基础架构\n","date":"2016-03-30","externalUrl":null,"permalink":"/posts/2016-03-30-introduce-kafka-architecture/","section":"文章","summary":"今天来聊下大数据场景下比较流行的消息队列组件 kafka。本篇文章将主要从理论角度来介绍。\nkafka 是一款开源、追求高吞吐、实时性，可持久化的流式消息队列，可同时处理在线（消息）与离线应用(业务数据和日志)。在如今火热的大数据时代，得到了广泛的应用。\n","title":"快速了解 Kafka 基础架构","type":"posts"},{"content":" cnposts # 中文博客内容\n","externalUrl":null,"permalink":"/readme/","section":"POLOXUE's BLOG","summary":"cnposts # 中文博客内容\n","title":"","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/docs/backtrader/","section":"教程","summary":"","title":"Backtrader 中文教程","type":"docs"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/","section":"POLOXUE's BLOG","summary":"","title":"POLOXUE's BLOG","type":"page"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/about/","section":"关于我","summary":"","title":"关于我","type":"about"},{"content":"POLOXUE - 十三年编程经验的程序员一枚。\n我的社交媒体：掘金，知乎，CSDN\n","externalUrl":null,"permalink":"/about/about/","section":"关于我","summary":"POLOXUE - 十三年编程经验的程序员一枚。\n我的社交媒体：掘金，知乎，CSDN\n","title":"关于我","type":"about"},{"content":"这里汇集了各种技术教程与文档。\n","externalUrl":null,"permalink":"/docs/","section":"教程","summary":"这里汇集了各种技术教程与文档。\n","title":"教程","type":"docs"},{"content":"","externalUrl":null,"permalink":"/posts/","section":"文章","summary":"","title":"文章","type":"posts"}]