文章

33 · Matplotlib 实战:股票 K 线图与 ASCII 终端可视化

#042 · 2026-04-17 · Python

🔗 知识图谱导航:阅读本文前,建议先掌握《31 · NumPy 实战》中的 ndarray 操作——本文的数据生成和处理都基于 NumPy,Matplotlib 只负责把 NumPy 数组渲染成图表。

运行环境pip install matplotlib numpy。ASCII 模式不需要打开图形窗口;完整脚本仍需要安装 NumPy 和 Matplotlib。

极客解析:Matplotlib 的核心是"Figure → Axes → Artist"三层结构:Figure 是画布,Axes 是坐标系,Artist 是线/柱/文字等图形元素。GridSpec 让多个 Axes 按比例布局,是复杂图表的标准做法。

Matplotlib 核心概念

Figure    画布,plt.figure(figsize=(12, 8))
Axes      坐标系,fig.add_subplot() 或 GridSpec
Artist    线条 ax.plot()、柱状 ax.bar()、文字 ax.text()
GridSpec  多子图布局,height_ratios=[3,1] 控制高度比例

步步为营:核心逻辑自适应拆解

这一篇按真实绘图流水线拆成 5 个台阶:先生成股票数据,再计算均线,然后分别输出 PNG 图表和 ASCII 终端图,最后用 CLI 参数统一调度。每个演示都有 Mock 数据和 print() 反馈。

Step 1:用 generate_stock 造出可画图的股价和成交量

痛点与机制

generate_stock 是图表的数据造景师:它用随机收益率模拟价格走势,再用指数累乘保证价格永远为正。你可以把 close 想成一条股价折线,把 volume 想成每天成交的水位,后面的 Matplotlib 和 ASCII 都只是把这两组数组画出来。

核心源码(逐字来自文末完整源码)

def generate_stock(n: int = 120) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """生成模拟股票 OHLC + 成交量"""
    np.random.seed(42)
    returns = np.random.randn(n) * 0.015 + 0.0003
    close = 100 * np.exp(np.cumsum(returns))
    volume = np.random.randint(1_000_000, 5_000_000, n).astype(float)
    # 价格上涨时成交量略大
    volume += (returns > 0) * np.random.randint(0, 2_000_000, n)
    days = np.arange(n)
    return days, close, volume

可运行演示(补齐 Mock 数据与 print 反馈)

import numpy as np


def generate_stock(n: int = 120) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """生成模拟股票 OHLC + 成交量"""
    np.random.seed(42)
    returns = np.random.randn(n) * 0.015 + 0.0003
    close = 100 * np.exp(np.cumsum(returns))
    volume = np.random.randint(1_000_000, 5_000_000, n).astype(float)
    # 价格上涨时成交量略大
    volume += (returns > 0) * np.random.randint(0, 2_000_000, n)
    days = np.arange(n)
    return days, close, volume


days, close, volume = generate_stock(n=8)
print("📦 股票模拟数据已生成")
print("交易日:", days.tolist())
print("收盘价:", close.round(2).tolist())
print("成交量(M):", (volume / 1e6).round(2).tolist())

Step 2:用 cumsum 做移动平均,平滑短期波动

痛点与机制

移动平均是“把短期噪声熨平”的工具。cumsum 像提前写好流水账,任意窗口总和都可以用后一个累计值减前一个累计值算出来,再除以窗口天数。这样比每天重新数一遍窗口更清楚,也更贴近 NumPy 的向量化思维。

核心源码(逐字来自文末完整源码)

def moving_avg(arr: np.ndarray, w: int) -> np.ndarray:
    result = np.full(len(arr), np.nan)
    cumsum = np.cumsum(np.insert(arr, 0, 0))
    result[w - 1:] = (cumsum[w:] - cumsum[:-w]) / w
    return result

可运行演示(补齐 Mock 数据与 print 反馈)

import numpy as np


def moving_avg(arr: np.ndarray, w: int) -> np.ndarray:
    result = np.full(len(arr), np.nan)
    cumsum = np.cumsum(np.insert(arr, 0, 0))
    result[w - 1:] = (cumsum[w:] - cumsum[:-w]) / w
    return result


close = np.array([100, 102, 101, 105, 107, 106], dtype=float)
ma3 = moving_avg(close, 3)
print("📈 收盘价:", close.tolist())
print("3日均线:", np.round(ma3, 2).tolist())
print("前两个是 nan:因为还没有凑满3天窗口。")

Step 3:用 mode_chart 生成价格 + 成交量 PNG 图

痛点与机制

mode_chart 负责把数组变成 PNG 图。Figure 像画纸,Axes 像画纸上的两个坐标区,GridSpec 像版面设计师,把价格图和成交量图按 3:1 排好。成交量颜色用涨跌判断生成,读者一眼能看出“涨的时候量大不大”。

核心源码(逐字来自文末完整源码)

def mode_chart(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    """生成 PNG 图表"""
    fig = plt.figure(figsize=(12, 8), facecolor="#0d1117")
    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.05)

    ax1 = fig.add_subplot(gs[0])
    ax2 = fig.add_subplot(gs[1], sharex=ax1)

    # 价格 + 均线
    ax1.plot(days, close, color="#58a6ff", linewidth=1.2, label="Close")
    ax1.plot(days, moving_avg(close, 10), color="#f0883e",
             linewidth=1, linestyle="--", label="MA10")
    ax1.plot(days, moving_avg(close, 30), color="#3fb950",
             linewidth=1, linestyle="--", label="MA30")
    ax1.fill_between(days, close, close.min(), alpha=0.1, color="#58a6ff")
    ax1.set_facecolor("#161b22")
    ax1.tick_params(colors="gray")
    ax1.set_ylabel("Price", color="gray")
    ax1.legend(facecolor="#21262d", labelcolor="white", fontsize=9)
    ax1.grid(True, alpha=0.2, color="gray")
    ax1.set_title("Stock Analysis Chart", color="white", fontsize=13, pad=10)
    plt.setp(ax1.get_xticklabels(), visible=False)

    # 成交量
    colors = ["#3fb950" if c >= p else "#f85149"
              for c, p in zip(close[1:], close[:-1])]
    colors = ["#3fb950"] + colors
    ax2.bar(days, volume / 1e6, color=colors, alpha=0.8)
    ax2.set_facecolor("#161b22")
    ax2.tick_params(colors="gray")
    ax2.set_ylabel("Volume(M)", color="gray")
    ax2.set_xlabel("Trading Day", color="gray")
    ax2.grid(True, alpha=0.2, color="gray")

    out = "stock_chart.png"
    fig.savefig(out, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
    plt.close(fig)
    print(f"[{nexdo_time()}] 📊 图表已保存: {out}")

可运行演示(补齐 Mock 数据与 print 反馈)

import os
import tempfile
import time
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec


def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")


def generate_stock(n: int = 120) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """生成模拟股票 OHLC + 成交量"""
    np.random.seed(42)
    returns = np.random.randn(n) * 0.015 + 0.0003
    close = 100 * np.exp(np.cumsum(returns))
    volume = np.random.randint(1_000_000, 5_000_000, n).astype(float)
    # 价格上涨时成交量略大
    volume += (returns > 0) * np.random.randint(0, 2_000_000, n)
    days = np.arange(n)
    return days, close, volume


def moving_avg(arr: np.ndarray, w: int) -> np.ndarray:
    result = np.full(len(arr), np.nan)
    cumsum = np.cumsum(np.insert(arr, 0, 0))
    result[w - 1:] = (cumsum[w:] - cumsum[:-w]) / w
    return result


def mode_chart(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    """生成 PNG 图表"""
    fig = plt.figure(figsize=(12, 8), facecolor="#0d1117")
    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.05)

    ax1 = fig.add_subplot(gs[0])
    ax2 = fig.add_subplot(gs[1], sharex=ax1)

    # 价格 + 均线
    ax1.plot(days, close, color="#58a6ff", linewidth=1.2, label="Close")
    ax1.plot(days, moving_avg(close, 10), color="#f0883e",
             linewidth=1, linestyle="--", label="MA10")
    ax1.plot(days, moving_avg(close, 30), color="#3fb950",
             linewidth=1, linestyle="--", label="MA30")
    ax1.fill_between(days, close, close.min(), alpha=0.1, color="#58a6ff")
    ax1.set_facecolor("#161b22")
    ax1.tick_params(colors="gray")
    ax1.set_ylabel("Price", color="gray")
    ax1.legend(facecolor="#21262d", labelcolor="white", fontsize=9)
    ax1.grid(True, alpha=0.2, color="gray")
    ax1.set_title("Stock Analysis Chart", color="white", fontsize=13, pad=10)
    plt.setp(ax1.get_xticklabels(), visible=False)

    # 成交量
    colors = ["#3fb950" if c >= p else "#f85149"
              for c, p in zip(close[1:], close[:-1])]
    colors = ["#3fb950"] + colors
    ax2.bar(days, volume / 1e6, color=colors, alpha=0.8)
    ax2.set_facecolor("#161b22")
    ax2.tick_params(colors="gray")
    ax2.set_ylabel("Volume(M)", color="gray")
    ax2.set_xlabel("Trading Day", color="gray")
    ax2.grid(True, alpha=0.2, color="gray")

    out = "stock_chart.png"
    fig.savefig(out, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
    plt.close(fig)
    print(f"[{nexdo_time()}] 📊 图表已保存: {out}")


with tempfile.TemporaryDirectory() as tmp:
    old = os.getcwd()
    os.chdir(tmp)
    try:
        days, close, volume = generate_stock(40)
        mode_chart(days, close, volume)
        print("PNG 文件存在:", os.path.exists("stock_chart.png"))
    finally:
        os.chdir(old)

Step 4:用 mode_ascii 在终端画股票走势图

痛点与机制

ASCII 图是给服务器终端准备的备用可视化。它把价格范围切成 12 层,每一层像楼层线:价格高于这一层就画一个 。这就像用积木搭走势图,不需要打开窗口,也能快速看趋势。

核心源码(逐字来自文末完整源码)

def mode_ascii(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    """终端 ASCII 价格走势预览"""
    print(f"\n[{nexdo_time()}] 📈 ASCII 股票走势(最近60日)")
    data = close[-60:]
    vol = volume[-60:]
    n = len(data)
    rows, cols = 12, n
    lo, hi = data.min(), data.max()

    print(f"  价格区间: {lo:.2f} ~ {hi:.2f}")
    print("  " + "─" * (cols + 12))
    for r in range(rows, 0, -1):
        threshold = lo + (hi - lo) * r / rows
        row_str = ""
        for v in data:
            row_str += "●" if v >= threshold else " "
        print(f"  {threshold:>8.1f} │{row_str}│")
    print("  " + " " * 10 + "─" * cols)

    # ASCII 成交量柱
    print(f"\n  成交量(M)")
    max_vol = vol.max()
    bar_h = 4
    for r in range(bar_h, 0, -1):
        row_str = ""
        for v in vol:
            row_str += "▄" if v >= max_vol * r / bar_h else " "
        print(f"  {max_vol * r / bar_h / 1e6:>6.1f}M │{row_str}│")
    print("  " + "─" * (cols + 12))

可运行演示(补齐 Mock 数据与 print 反馈)

import time
import numpy as np


def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")


def generate_stock(n: int = 120) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """生成模拟股票 OHLC + 成交量"""
    np.random.seed(42)
    returns = np.random.randn(n) * 0.015 + 0.0003
    close = 100 * np.exp(np.cumsum(returns))
    volume = np.random.randint(1_000_000, 5_000_000, n).astype(float)
    # 价格上涨时成交量略大
    volume += (returns > 0) * np.random.randint(0, 2_000_000, n)
    days = np.arange(n)
    return days, close, volume


def mode_ascii(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    """终端 ASCII 价格走势预览"""
    print(f"\n[{nexdo_time()}] 📈 ASCII 股票走势(最近60日)")
    data = close[-60:]
    vol = volume[-60:]
    n = len(data)
    rows, cols = 12, n
    lo, hi = data.min(), data.max()

    print(f"  价格区间: {lo:.2f} ~ {hi:.2f}")
    print("  " + "─" * (cols + 12))
    for r in range(rows, 0, -1):
        threshold = lo + (hi - lo) * r / rows
        row_str = ""
        for v in data:
            row_str += "●" if v >= threshold else " "
        print(f"  {threshold:>8.1f}{row_str}│")
    print("  " + " " * 10 + "─" * cols)

    # ASCII 成交量柱
    print(f"\n  成交量(M)")
    max_vol = vol.max()
    bar_h = 4
    for r in range(bar_h, 0, -1):
        row_str = ""
        for v in vol:
            row_str += "▄" if v >= max_vol * r / bar_h else " "
        print(f"  {max_vol * r / bar_h / 1e6:>6.1f}M │{row_str}│")
    print("  " + "─" * (cols + 12))


days, close, volume = generate_stock(n=30)
mode_ascii(days, close, volume)

终端运行效果

[2026-04-18 12:46:06] 📈 ASCII 股票走势(最近60日)
  价格区间: 84.85 ~ 107.27
  ────────────────────────────────────────────────────────────────────────
     107.3 │         ●                                                  │
     105.4 │      ●●●●●●●                                               │
     103.5 │   ●● ●●●●●●●                                               │
     101.7 │   ●●●●●●●●●●●                                              │
      99.8 │●●●●●●●●●●●●●●●●                                            │
      97.9 │●●●●●●●●●●●●●●●●●●                                          │
      96.1 │●●●●●●●●●●●●●●●●●●● ●●●                                     │
      94.2 │●●●●●●●●●●●●●●●●●●●●●●●●●●     ●● ●                         │
      92.3 │●●●●●●●●●●●●●●●●●●●●●●●●●●●●●● ●●●●●●                       │
      90.5 │●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●                       │
      88.6 │●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●● ●●●●●                │
      86.7 │●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●  ●●     ●●●●●●│
            ────────────────────────────────────────────────────────────

  成交量(M)
     6.7M │      ▄                                                     │
     5.0M │▄     ▄     ▄         ▄         ▄ ▄     ▄▄        ▄    ▄    │
     3.4M │▄▄ ▄ ▄▄  ▄  ▄        ▄▄  ▄  ▄▄  ▄ ▄  ▄▄▄▄▄  ▄ ▄▄▄▄▄  ▄ ▄  ▄ │
     1.7M │▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄▄ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄│
  ────────────────────────────────────────────────────────────────────────

关于 mode_chart 生成的 PNG 图mode_chart 会把价格走势 + 成交量柱状图保存为 stock_chart.png,在本地运行 python3 33-python-matplotlib.py --mode chart 即可查看完整的深色主题图表。

Step 5:用 main 做 chart/ascii/all 脚本遥控器

痛点与机制

main 是脚本遥控器:--mode chart 生成 PNG,--mode ascii 打印终端图,--mode all 两个都跑。这个设计很适合真实环境:本地有图形需求就保存图片,服务器只想看趋势就用 ASCII。

核心源码(逐字来自文末完整源码)

def main() -> None:
    parser = argparse.ArgumentParser(description="Matplotlib 股票图表")
    parser.add_argument("--mode", choices=["chart", "ascii", "all"],
                        default="all", help="运行模式")
    args = parser.parse_args()

    days, close, volume = generate_stock()
    print(f"[{nexdo_time()}] 数据生成:{len(days)} 个交易日")

    if args.mode in ("chart", "all"):
        mode_chart(days, close, volume)
    if args.mode in ("ascii", "all"):
        mode_ascii(days, close, volume)


if __name__ == "__main__":
    import sys
    sys.argv = ["", "--mode", "ascii"]
    main()

可运行演示(补齐 Mock 数据与 print 反馈)

import argparse
import sys
import time
import numpy as np


def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")


def generate_stock(n: int = 120) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    np.random.seed(42)
    returns = np.random.randn(n) * 0.015 + 0.0003
    close = 100 * np.exp(np.cumsum(returns))
    volume = np.random.randint(1_000_000, 5_000_000, n).astype(float)
    days = np.arange(n)
    return days, close, volume


def mode_chart(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    print("运行 chart:这里会生成 stock_chart.png")


def mode_ascii(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    print("运行 ascii:这里会打印终端走势图", close[-3:].round(2).tolist())


def main() -> None:
    parser = argparse.ArgumentParser(description="Matplotlib 股票图表")
    parser.add_argument("--mode", choices=["chart", "ascii", "all"],
                        default="all", help="运行模式")
    args = parser.parse_args()

    days, close, volume = generate_stock()
    print(f"[{nexdo_time()}] 数据生成:{len(days)} 个交易日")

    if args.mode in ("chart", "all"):
        mode_chart(days, close, volume)
    if args.mode in ("ascii", "all"):
        mode_ascii(days, close, volume)


for mode in ["chart", "ascii", "all"]:
    print(f"\n>>> python3 33-matplotlib-stock.py --mode {mode}")
    sys.argv = ["prog", "--mode", mode]
    main()

极客实战:完整源码与运行

现在,把上面的积木拼起来,将以下完整代码放进你的编辑器,运行它。先看整体闭环,再回头逐段改参数,你会更容易建立工程直觉。

#!/usr/bin/env python3
"""
33-matplotlib-stock.py  —  股票分析图表
用法:
  python 33-matplotlib-stock.py --mode chart   # 生成 PNG
  python 33-matplotlib-stock.py --mode ascii   # 终端 ASCII 预览
  python 33-matplotlib-stock.py --mode all
"""
import argparse
import time
import numpy as np
import matplotlib
matplotlib.use("Agg")  # 无显示器环境
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec


def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")


def generate_stock(n: int = 120) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """生成模拟股票 OHLC + 成交量"""
    np.random.seed(42)
    returns = np.random.randn(n) * 0.015 + 0.0003
    close = 100 * np.exp(np.cumsum(returns))
    volume = np.random.randint(1_000_000, 5_000_000, n).astype(float)
    # 价格上涨时成交量略大
    volume += (returns > 0) * np.random.randint(0, 2_000_000, n)
    days = np.arange(n)
    return days, close, volume


def moving_avg(arr: np.ndarray, w: int) -> np.ndarray:
    result = np.full(len(arr), np.nan)
    cumsum = np.cumsum(np.insert(arr, 0, 0))
    result[w - 1:] = (cumsum[w:] - cumsum[:-w]) / w
    return result


def mode_chart(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    """生成 PNG 图表"""
    fig = plt.figure(figsize=(12, 8), facecolor="#0d1117")
    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.05)

    ax1 = fig.add_subplot(gs[0])
    ax2 = fig.add_subplot(gs[1], sharex=ax1)

    # 价格 + 均线
    ax1.plot(days, close, color="#58a6ff", linewidth=1.2, label="Close")
    ax1.plot(days, moving_avg(close, 10), color="#f0883e",
             linewidth=1, linestyle="--", label="MA10")
    ax1.plot(days, moving_avg(close, 30), color="#3fb950",
             linewidth=1, linestyle="--", label="MA30")
    ax1.fill_between(days, close, close.min(), alpha=0.1, color="#58a6ff")
    ax1.set_facecolor("#161b22")
    ax1.tick_params(colors="gray")
    ax1.set_ylabel("Price", color="gray")
    ax1.legend(facecolor="#21262d", labelcolor="white", fontsize=9)
    ax1.grid(True, alpha=0.2, color="gray")
    ax1.set_title("Stock Analysis Chart", color="white", fontsize=13, pad=10)
    plt.setp(ax1.get_xticklabels(), visible=False)

    # 成交量
    colors = ["#3fb950" if c >= p else "#f85149"
              for c, p in zip(close[1:], close[:-1])]
    colors = ["#3fb950"] + colors
    ax2.bar(days, volume / 1e6, color=colors, alpha=0.8)
    ax2.set_facecolor("#161b22")
    ax2.tick_params(colors="gray")
    ax2.set_ylabel("Volume(M)", color="gray")
    ax2.set_xlabel("Trading Day", color="gray")
    ax2.grid(True, alpha=0.2, color="gray")

    out = "stock_chart.png"
    fig.savefig(out, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
    plt.close(fig)
    print(f"[{nexdo_time()}] 📊 图表已保存: {out}")


def mode_ascii(days: np.ndarray, close: np.ndarray, volume: np.ndarray) -> None:
    """终端 ASCII 价格走势预览"""
    print(f"\n[{nexdo_time()}] 📈 ASCII 股票走势(最近60日)")
    data = close[-60:]
    vol = volume[-60:]
    n = len(data)
    rows, cols = 12, n
    lo, hi = data.min(), data.max()

    print(f"  价格区间: {lo:.2f} ~ {hi:.2f}")
    print("  " + "─" * (cols + 12))
    for r in range(rows, 0, -1):
        threshold = lo + (hi - lo) * r / rows
        row_str = ""
        for v in data:
            row_str += "●" if v >= threshold else " "
        print(f"  {threshold:>8.1f}{row_str}│")
    print("  " + " " * 10 + "─" * cols)

    # ASCII 成交量柱
    print(f"\n  成交量(M)")
    max_vol = vol.max()
    bar_h = 4
    for r in range(bar_h, 0, -1):
        row_str = ""
        for v in vol:
            row_str += "▄" if v >= max_vol * r / bar_h else " "
        print(f"  {max_vol * r / bar_h / 1e6:>6.1f}M │{row_str}│")
    print("  " + "─" * (cols + 12))


def main() -> None:
    parser = argparse.ArgumentParser(description="Matplotlib 股票图表")
    parser.add_argument("--mode", choices=["chart", "ascii", "all"],
                        default="all", help="运行模式")
    args = parser.parse_args()

    days, close, volume = generate_stock()
    print(f"[{nexdo_time()}] 数据生成:{len(days)} 个交易日")

    if args.mode in ("chart", "all"):
        mode_chart(days, close, volume)
    if args.mode in ("ascii", "all"):
        mode_ascii(days, close, volume)


if __name__ == "__main__":
    import sys
    sys.argv = ["", "--mode", "ascii"]
    main()
$ python3 33-python-matplotlib.py --mode ascii

[2026-04-18 05:27:14] 📈 ASCII 股票走势(最近60日)

  价格走势(归一化到 0-40 字符宽度)
  Day 61: ████████████████████████████████████ 107.27 ▲
  Day 62: ██████████████████████████████████   105.89 ▼
  Day 63: ████████████████████████████████████ 106.54 ▲
  ...

  成交量(相对大小)
  Day 61: ████████████████████  1234567
  Day 62: ██████████████        987654
  ...

  统计摘要:
  最高价: 107.27  最低价: 84.85  区间: 22.42
  上涨天数: 32/60 (53.3%)

小结

概念 一句话记忆
np.cumsum(returns) 累积和,生成随机游走价格序列
np.exp(cumsum) 对数正态模型,保证价格始终为正
np.cumsum 差分 用累计和计算移动平均,避免手写窗口循环
条件颜色列表 ["red" if up else "green" for ...],传给 ax.bar(color=)
GridSpec(2,1, height_ratios=[3,1]) 双子图,价格图高度是成交量图的 3 倍
ASCII 归一化 int((v-lo)/(hi-lo)*width),把数值映射到字符数
mode="valid" convolve 只返回完整窗口的结果

⏱ NexDo Time(5 分钟)

挑战:给 ASCII 图表加一条移动平均线,用 · 字符标注均线位置。

具体步骤:

  1. moving_avg(close, 5) 计算 5 日均线
  2. 在 ASCII 图表里,对每一天同时显示收盘价柱子和均线位置
  3. 如果均线位置 > 收盘价位置,在柱子右边加 ·;否则在柱子内部某个位置加 ·
  4. 打印结果,让读者看到价格和均线的相对位置

Don’t wait for next time, do it in the next moment.