Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PART II 進階部分 - 量化實戰(Rust Quantitative Trading in Actions)

Chapter 23 用Polars實現並加速數據框架處理

23.1 Rust與數據框架處理工具Polars

經過以上的學習,我們很自然地知道,Rust 的編譯器通過嚴格的編譯檢查和優化,能夠生成接近於手寫彙編的高效代碼。它的零成本抽象特性確保了高效的運行時性能,非常適合處理大量數據和計算密集型任務。同時,Rust 提供了獨特的所有權系統和借用檢查器,能夠防止數據競爭和內存洩漏。這些特性使得開發者可以編寫更安全的多線程數據處理代碼,減少併發錯誤的發生。另外,Rust 的併發模型使得編寫高效的並行代碼變得更加簡單和安全。通過使用 Tokio 等異步編程框架,開發者可以高效地處理大量併發任務,提升數據處理的吞吐量。所以使用 Rust 進行數據處理,結合其性能、安全性、併發支持和跨平臺兼容性,我們能夠構建出高效、可靠和靈活的數據處理工具,滿足現代數據密集型應用的需求。本節將以Poars為例教讀者如何實現並加速數據框架處理。

Polars 簡介

Polars 起初是一個在2020年作為愛好項目開始的開源庫,但很快在開源社區中獲得了廣泛關注。許多開發者一直在尋找一個既易用又高性能的 DataFrame 庫,Polars 正是為了填補這一空缺而出現的。隨著越來越多來自不同背景和編程語言的貢獻者加入,Polars 社區迅速壯大。由於社區的巨大努力,Polars 現在正式支持三種語言(Rust、Python、JS),並計劃支持兩種新的語言(R、Ruby)。

哲學理念

Polars 的目標是提供一個極速的 DataFrame 庫,其特點包括:

  • 利用機器上的所有可用核心。
  • 優化查詢以減少不必要的工作和內存分配。
  • 處理比可用內存更大的數據集。
  • 提供一致且可預測的 API。
  • 遵循嚴格的模式(在運行查詢前應已知數據類型)。

Polars 使用 Rust 編寫,具有 C/C++ 的性能,並能完全控制查詢引擎中性能關鍵的部分。

主要功能

  • 快速:從頭開始用 Rust 編寫,設計緊貼機器且無外部依賴。
  • I/O 支持:對本地、雲存儲和數據庫的所有常見數據存儲層提供一流支持。
  • 直觀的 API:以自然的方式編寫查詢,Polars 內部會通過查詢優化器確定最有效的執行方式。
  • Out of Core:流式 API 允許處理結果時不需要將所有數據同時加載到內存中。
  • 並行處理:利用多核 CPU,無需額外配置即可分配工作負載。
  • 向量化查詢引擎:使用 Apache Arrow 列式數據格式,以向量化方式處理查詢,優化 CPU 使用。
  • LazyMode:支持延遲計算模式,通過鏈式調用優化性能和資源使用。
  • PyO3 支持:通過 PyO3 提供對 Python 的強大支持,使研究人員可以方便地使用 Python 進行數據分析。

在接下來的章節中,我們會頻繁接觸到這些Polars先進的特性。

Rust 中的數據處理框架

  1. DataFusion:DataFusion 是一個用於查詢和數據處理的高性能查詢引擎,支持 SQL 查詢語法,並能夠與 Arrow 格式的數據無縫集成,適用於大規模數據處理和分析。
  2. Arrow:Apache Arrow 是一個跨語言的開發平臺,旨在實現高性能的列式內存格式,支持高效的數據序列化和反序列化操作,廣泛應用於大數據處理和數據分析領域。

這些其他也框架各有特點,為 Rust 開發者提供了豐富的數據處理和分析工具,能夠滿足不同的應用需求。

23.2 開始使用Polars

23.2.1 為項目加入polars庫

本章節旨在幫助您開始使用 Polars。它涵蓋了該庫的所有基本功能和特性,使新用戶能夠輕鬆熟悉從初始安裝和設置到核心功能的基礎知識。如果您已經是高級用戶或熟悉 DataFrame,您可以跳過本章節,直接進入下一個章節瞭解安裝選項。

# 為項目加入polars庫並且打開 'lazy' flag
cargo add polars -F lazy

# Or Cargo.toml
[dependencies]
polars = { version = "x", features = ["lazy", ...]}

23.2.2 讀取與寫入

Polars 支持讀取和寫入常見文件格式(如 csv、json、parquet)、雲存儲(S3、Azure Blob、BigQuery)和數據庫(如 postgres、mysql)。以下示例展示了在磁盤上讀取和寫入的概念。

#![allow(unused)]
fn main() {
use std::fs::File; // 導入文件系統模塊
use chrono::prelude::*; // 導入 Chrono 時間庫
use polars::prelude::*; // 導入 Polars 庫

// 創建一個 DataFrame,包含四列數據:整數、日期、浮點數和字符串
let mut df: DataFrame = df!(
    "integer" => &[1, 2, 3], // 整數列
    "date" => &[ // 日期列
        NaiveDate::from_ymd_opt(2025, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(), // 第一天
        NaiveDate::from_ymd_opt(2025, 1, 2).unwrap().and_hms_opt(0, 0, 0).unwrap(), // 第二天
        NaiveDate::from_ymd_opt(2025, 1, 3).unwrap().and_hms_opt(0, 0, 0).unwrap(), // 第三天
    ],
    "float" => &[4.0, 5.0, 6.0], // 浮點數列
    "string" => &["a", "b", "c"], // 字符串列
)
.unwrap(); // 創建 DataFrame 成功後,解除 Result 包裝

// 打印 DataFrame 的內容
println!("{}", df);
}

這段代碼展示瞭如何使用 Polars 在 Rust 中創建一個 DataFrame 並打印其內容。DataFrame 包含四列數據,分別是整數、日期、浮點數和字符串。通過這種方式,開發者可以方便地處理和分析數據。

shape: (3, 4)
┌─────────┬─────────────────────┬───────┬────────┐
│ integer ┆ date                ┆ float ┆ string │
│ ---     ┆ ---                 ┆ ---   ┆ ---    │
│ i64     ┆ datetime[μs]        ┆ f64   ┆ str    │
╞═════════╪═════════════════════╪═══════╪════════╡
│ 1       ┆ 2025-01-01 00:00:00 ┆ 4.0   ┆ a      │
│ 2       ┆ 2025-01-02 00:00:00 ┆ 5.0   ┆ b      │
│ 3       ┆ 2025-01-03 00:00:00 ┆ 6.0   ┆ c      │
└─────────┴─────────────────────┴───────┴────────┘

23.2.3 Polars 表達式

Polars 的表達式是其核心優勢之一,提供了模塊化結構,使得簡單概念可以組合成複雜查詢。以下是構建所有查詢的基本組件:

  • select
  • filter
  • with_columns
  • group_by

要了解更多關於表達式和它們操作的上下文,請參閱用戶指南中的上下文和表達式部分。

23.2.3.1 選擇(Select)

選擇一列數據需要做兩件事:

  1. 定義我們要獲取數據的 DataFrame。
  2. 選擇所需的數據。

在下面的示例中,我們選擇 col('*'),星號代表所有列。

Rust 示例代碼

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 假設 df 是已創建的 DataFrame
let out = df.clone().lazy().select([col("*")]).collect()?;
println!("{}", out);
}

輸出示例:

shape: (5, 4)
┌─────┬──────────┬─────────────────────┬───────┐
│ a   ┆ b        ┆ c                   ┆ d     │
│ --- ┆ ---      ┆ ---                 ┆ ---   │
│ i64 ┆ f64      ┆ datetime[μs]        ┆ f64   │
╞═════╪══════════╪═════════════════════╪═══════╡
│ 0   ┆ 0.10666  ┆ 2025-12-01 00:00:00 ┆ 1.0   │
│ 1   ┆ 0.596863 ┆ 2025-12-02 00:00:00 ┆ 2.0   │
│ 2   ┆ 0.691304 ┆ 2025-12-03 00:00:00 ┆ NaN   │
│ 3   ┆ 0.906636 ┆ 2025-12-04 00:00:00 ┆ -42.0 │
│ 4   ┆ 0.101216 ┆ 2025-12-05 00:00:00 ┆ null  │
└─────┴──────────┴─────────────────────┴───────┘

你也可以指定要返回的特定列,以下是傳遞列名的方式。

Rust 示例代碼

#![allow(unused)]
fn main() {
use polars::prelude::*;

let out = df.clone().lazy().select([col("a"), col("b")]).collect()?;
println!("{}", out);
}

輸出示例:

shape: (5, 2)
┌─────┬──────────┐
│ a   ┆ b        │
│ --- ┆ ---      │
│ i64 ┆ f64      │
╞═════╪══════════╡
│ 0   ┆ 0.10666  │
│ 1   ┆ 0.596863 │
│ 2   ┆ 0.691304 │
│ 3   ┆ 0.906636 │
│ 4   ┆ 0.101216 │
└─────┴──────────┘

23.2.3.2 過濾(Filter)

過濾選項允許我們創建 DataFrame 的子集。我們使用之前的 DataFrame,並在兩個指定日期之間進行過濾。

Rust 示例代碼

下面的示例展示瞭如何使用 Polars 和 Rust 進行數據過濾操作。我們將基於兩個指定日期對 DataFrame 進行過濾。

#![allow(unused)]
fn main() {
use polars::prelude::*;
use chrono::NaiveDate;

let start_date = NaiveDate::from_ymd(2025, 12, 2).and_hms(0, 0, 0); // 定義開始日期
let end_date = NaiveDate::from_ymd(2025, 12, 3).and_hms(0, 0, 0);   // 定義結束日期

let out = df.clone().lazy().filter( // 創建 DataFrame 的一個副本,並進入惰性計算模式
    col("c").gt_eq(lit(start_date)) // 過濾條件:列 "c" 的值大於等於開始日期
    .and(col("c").lt_eq(lit(end_date))) // 過濾條件:列 "c" 的值小於等於結束日期
).collect()?; // 收集結果並執行計算

println!("{}", out); // 打印過濾後的 DataFrame

}

**注意。**在這裡lit() 全稱是 literal。在 Polars 中,lit() 函數用於將一個常量值轉換為 Polars 表達式,使其可以在查詢中使用。

示例代碼的輸出示例如下:

shape: (2, 4)
┌─────┬──────────┬─────────────────────┬─────┐
│ a   ┆ b        ┆ c                   ┆ d   │
│ --- ┆ ---      ┆ ---                 ┆ --- │
│ i64 ┆ f64      ┆ datetime[μs]        ┆ f64 │
╞═════╪══════════╪═════════════════════╪═════╡
│ 1   ┆ 0.596863 ┆ 2025-12-02 00:00:00 ┆ 2.0 │
│ 2   ┆ 0.691304 ┆ 2025-12-03 00:00:00 ┆ NaN │
└─────┴──────────┴─────────────────────┴─────┘

你還可以創建包含多個列的更復雜的過濾器。

Rust 示例代碼

下面的示例展示瞭如何使用 Polars 和 Rust 進行數據過濾操作。我們將基於一個條件對 DataFrame 進行過濾。

#![allow(unused)]
fn main() {
use polars::prelude::*;

let out = df.clone().lazy().filter(
    col("a").lt_eq(3) // 過濾條件:列 "a" 的值小於或等於 3
    .and(col("d").is_not_null()) // 過濾條件:列 "d" 的值不是空值
).collect()?; // 收集結果並執行計算

println!("{}", out); // 打印過濾後的 DataFrame
}

輸出示例:

shape: (3, 4)
┌─────┬──────────┬─────────────────────┬───────┐
│ a   ┆ b        ┆ c                   ┆ d     │
│ --- ┆ ---      ┆ ---                 ┆ ---   │
│ i64 ┆ f64      ┆ datetime[μs]        ┆ f64   │
╞═════╪══════════╪═════════════════════╪═══════╡
│ 0   ┆ 0.10666  ┆ 2025-12-01 00:00:00 ┆ 1.0   │
│ 1   ┆ 0.596863 ┆ 2025-12-02 00:00:00 ┆ 2.0   │
│ 3   ┆ 0.906636 ┆ 2025-12-04 00:00:00 ┆ -42.0 │
└─────┴──────────┴─────────────────────┴───────┘

23.2.3.3 添加列(Add Columns)

with_columns 允許你為分析創建新列。我們將創建兩個新列 eb+42。首先,我們將列 b 的所有值求和並存儲在新列 e 中。然後我們將列 b 的值加上 42,並將結果存儲在新列 b+42 中。

Rust 示例代碼

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 創建新的列
let out = df
    .clone() // 克隆 DataFrame
    .lazy() // 進入惰性計算模式
    .with_columns([
        col("b").sum().alias("e"), // 新列 e:列 b 的所有值求和
        (col("b") + lit(42)).alias("b+42"), // 新列 b+42:列 b 的值加 42
    ])
    .collect()?; // 收集結果並執行計算

println!("{}", out); // 打印結果
}

輸出示例:

shape: (5, 6)
┌─────┬──────────┬─────────────────────┬───────┬──────────┬───────────┐
│ a   ┆ b        ┆ c                   ┆ d     ┆ e        ┆ b+42      │
│ --- ┆ ---      ┆ ---                 ┆ ---   ┆ ---      ┆ ---       │
│ i64 ┆ f64      ┆ datetime[μs]        ┆ f64   ┆ f64      ┆ f64       │
╞═════╪══════════╪═════════════════════╪═══════╪══════════╪═══════════╡
│ 0   ┆ 0.10666  ┆ 2025-12-01 00:00:00 ┆ 1.0   ┆ 2.402679 ┆ 42.10666  │
│ 1   ┆ 0.596863 ┆ 2025-12-02 00:00:00 ┆ 2.0   ┆ 2.402679 ┆ 42.596863 │
│ 2   ┆ 0.691304 ┆ 2025-12-03 00:00:00 ┆ NaN   ┆ 2.402679 ┆ 42.691304 │
│ 3   ┆ 0.906636 ┆ 2025-12-04 00:00:00 ┆ -42.0 ┆ 2.402679 ┆ 42.906636 │
│ 4   ┆ 0.101216 ┆ 2025-12-05 00:00:00 ┆ null  ┆ 2.402679 ┆ 42.101216 │
└─────┴──────────┴─────────────────────┴───────┴──────────┴───────────┘

23.2.3.4 分組(Group by)

我們將創建一個新的 DataFrame 來演示分組功能。這個新的 DataFrame 包含多個“組”,我們將按這些組進行分組。

創建 DataFrame

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 創建 DataFrame
let df2: DataFrame = df!("x" => 0..8, "y"=> &["A", "A", "A", "B", "B", "C", "X", "X"]).expect("should not fail");
println!("{}", df2);
}

輸出示例:

shape: (8, 2)
┌─────┬─────┐
│ x   ┆ y   │
│ --- ┆ --- │
│ i64 ┆ str │
╞═════╪═════╡
│ 0   ┆ A   │
│ 1   ┆ A   │
│ 2   ┆ A   │
│ 3   ┆ B   │
│ 4   ┆ B   │
│ 5   ┆ C   │
│ 6   ┆ X   │
│ 7   ┆ X   │
└─────┴─────┘

分組並聚合

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 按列 "y" 進行分組,並聚合
let out = df2.clone().lazy().group_by(["y"]).agg([len()]).collect()?;
println!("{}", out);
}

輸出示例:

shape: (4, 2)
┌─────┬─────┐
│ y   ┆ len │
│ --- ┆ --- │
│ str ┆ u32 │
╞═════╪═════╡
│ A   ┆ 3   │
│ B   ┆ 2   │
│ C   ┆ 1   │
│ X   ┆ 2   │
└─────┴─────┘
#![allow(unused)]
fn main() {
use polars::prelude::*;

// 按列 "y" 進行分組,並聚合多個統計量
let out = df2
    .clone()
    .lazy()
    .group_by(["y"])
    .agg([col("*").count().alias("count"), col("*").sum().alias("sum")])
    .collect()?;
println!("{}", out);
}

輸出示例:

shape: (4, 3)
┌─────┬───────┬─────┐
│ y   ┆ count ┆ sum │
│ --- ┆ ---   ┆ --- │
│ str ┆ u32   ┆ i64 │
╞═════╪═══════╪═════╡
│ A   ┆ 3     ┆ 3   │
│ B   ┆ 2     ┆ 7   │
│ C   ┆ 1     ┆ 5   │
│ X   ┆ 2     ┆ 13  │
└─────┴───────┴─────┘

23.2.3.5 組合操作

以下示例展示瞭如何組合操作來創建所需的 DataFrame。

創建並選擇列(排除c、d列)

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 創建新列並選擇
let out = df
    .clone()
    .lazy()
    .with_columns([(col("a") * col("b")).alias("a * b")])
    .select([col("*").exclude(["c", "d"])])
    .collect()?;
println!("{}", out);
}

輸出示例:

shape: (5, 3)
┌─────┬──────────┬──────────┐
│ a   ┆ b        ┆ a * b    │
│ --- ┆ ---      ┆ ---      │
│ i64 ┆ f64      ┆ f64      │
╞═════╪══════════╪══════════╡
│ 0   ┆ 0.10666  ┆ 0.0      │
│ 1   ┆ 0.596863 ┆ 0.596863 │
│ 2   ┆ 0.691304 ┆ 1.382607 │
│ 3   ┆ 0.906636 ┆ 2.719909 │
│ 4   ┆ 0.101216 ┆ 0.404864 │
└─────┴──────────┴──────────┘

創建並選擇列(排除d列)

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 創建新列並選擇
let out = df
    .clone()
    .lazy()
    .with_columns([(col("a") * col("b")).alias("a * b")])
    .select([col("*").exclude(["d"])])
    .collect()?;
println!("{}", out);
}

輸出示例:

shape: (5, 4)
┌─────┬──────────┬─────────────────────┬──────────┐
│ a   ┆ b        ┆ c                   ┆ a * b    │
│ --- ┆ ---      ┆ ---                 ┆ ---      │
│ i64 ┆ f64      ┆ datetime[μs]        ┆ f64      │
╞═════╪══════════╪═════════════════════╪══════════╡
│ 0   ┆ 0.10666  ┆ 2025-12-01 00:00:00 ┆ 0.0      │
│ 1   ┆ 0.596863 ┆ 2025-12-02 00:00:00 ┆ 0.596863 │
│ 2   ┆ 0.691304 ┆ 2025-12-03 00:00:00 ┆ 1.382607 │
│ 3   ┆ 0.906636 ┆ 2025-12-04 00:00:00 ┆ 2.719909 │
│ 4   ┆ 0.101216 ┆ 2025-12-05 00:00:00 ┆ 0.404864 │
└─────┴──────────┴─────────────────────┴──────────┘

23.2.4 合併 DataFrames

根據使用情況,DataFrames 可以通過兩種方式進行合併:joinconcat

23.2.4.1 連接(Join)

數據表連接類型詳解

在數據分析中,連接(Join)操作用於將兩個 DataFrames 合併。Polars 支持多種連接類型,包括左連接(Left Join)、右連接(Right Join)、內連接(Inner Join)和外連接(Outer Join)。以下是每種連接類型的詳細介紹和示例。

左連接(Left Join)

左連接返回左表中的所有行以及與右表中匹配的行。如果右表中沒有匹配的行,則結果中的相應列為 NULL。

#![allow(unused)]
fn main() {
use polars::prelude::*;
use rand::Rng;

let mut rng = rand::thread_rng();

let df1: DataFrame = df!(
    "a" => 0..8,
    "b" => (0..8).map(|_| rng.gen::<f64>()).collect::<Vec<f64>>()
).unwrap();

let df2: DataFrame = df!(
    "x" => 0..8,
    "y" => &["A", "A", "A", "B", "B", "C", "X", "X"]
).unwrap();

let joined = df1.join(&df2, ["a"], ["x"], JoinType::Left.into())?;
println!("{}", joined);
}

輸出示例:

shape: (8, 4)
┌─────┬──────────┬───────┬─────┐
│ a   ┆ b        ┆ x     ┆ y   │
│ --- ┆ ---      ┆ ---   ┆ --- │
│ i64 ┆ f64      ┆ i64   ┆ str │
╞═════╪══════════╪═══════╪═════╡
│ 0   ┆ 0.495791 ┆ 0     ┆ A   │
│ 1   ┆ 0.786293 ┆ 1     ┆ A   │
│ 2   ┆ 0.847485 ┆ 2     ┆ A   │
│ 3   ┆ 0.839398 ┆ 3     ┆ B   │
│ 4   ┆ 0.060646 ┆ 4     ┆ B   │
│ 5   ┆ 0.251472 ┆ 5     ┆ C   │
│ 6   ┆ 0.13899  ┆ 6     ┆ X   │
│ 7   ┆ 0.676241 ┆ 7     ┆ X   │
└─────┴──────────┴───────┴─────┘

右連接(Right Join)

右連接返回右表中的所有行以及與左表中匹配的行。如果左表中沒有匹配的行,則結果中的相應列為 NULL。

#![allow(unused)]
fn main() {
let joined = df1.join(&df2, ["a"], ["x"], JoinType::Right.into())?;
println!("{}", joined);
}

內連接(Inner Join)

內連接僅返回兩個表中匹配的行。如果沒有匹配的行,則該行不出現在結果中。

#![allow(unused)]
fn main() {
let joined = df1.join(&df2, ["a"], ["x"], JoinType::Inner.into())?;
println!("{}", joined);
}

外連接(Outer Join)

外連接返回兩個表中的所有行。如果一張表中沒有匹配的行,則結果中的相應列為 NULL。

#![allow(unused)]
fn main() {
let joined = df1.join(&df2, ["a"], ["x"], JoinType::Outer.into())?;
println!("{}", joined);
}

示例代碼解釋

  1. 數據生成

    #![allow(unused)]
    fn main() {
    let df1: DataFrame = df!(
        "a" => 0..8,
        "b" => (0..8).map(|_| rng.gen::<f64>()).collect::<Vec<f64>>()
    ).unwrap();
    
    let df2: DataFrame = df!(
        "x" => 0..8,
        "y" => &["A", "A", "A", "B", "B", "C", "X", "X"]
    ).unwrap();
    }

    這段代碼創建了兩個 DataFrames,df1 包含列 abdf2 包含列 xy

  2. 連接操作

    #![allow(unused)]
    fn main() {
    let joined = df1.join(&df2, ["a"], ["x"], JoinType::Left.into())?;
    println!("{}", joined);
    }

    這段代碼執行了左連接,結果包含 df1 中的所有行以及 df2 中匹配的行。

通過這些示例,你可以更好地理解如何在 Rust 中使用 Polars 進行不同類型的連接操作。

23.2.4.2 粘連(Concat)

我們也可以粘連兩個 DataFrames。垂直粘連會使 DataFrame 變長,水平粘連會使 DataFrame 變寬。以下示例展示了水平粘連兩個 DataFrames 的結果。

Rust 示例代碼

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 水平連接兩個 DataFrames
let stacked = df.hstack(df2.get_columns())?;
println!("{}", stacked); // 打印連接後的 DataFrame
}

輸出示例:

shape: (8, 5)
┌─────┬──────────┬───────┬─────┬─────┐
│ a   ┆ b        ┆ d     ┆ x   ┆ y   │
│ --- ┆ ---      ┆ ---   ┆ --- ┆ --- │
│ i64 ┆ f64      ┆ f64   ┆ i64 ┆ str │
╞═════╪══════════╪═══════╪═════╪═════╡
│ 0   ┆ 0.495791 ┆ 1.0   ┆ 0   ┆ A   │
│ 1   ┆ 0.786293 ┆ 2.0   ┆ 1   ┆ A   │
│ 2   ┆ 0.847485 ┆ NaN   ┆ 2   ┆ A   │
│ 3   ┆ 0.839398 ┆ NaN   ┆ 3   ┆ B   │
│ 4   ┆ 0.060646 ┆ 0.0   ┆ 4   ┆ B   │
│ 5   ┆ 0.251472 ┆ -5.0  ┆ 5   ┆ C   │
│ 6   ┆ 0.13899  ┆ -42.0 ┆ 6   ┆ X   │
│ 7   ┆ 0.676241 ┆ null  ┆ 7   ┆ X   │
└─────┴──────────┴───────┴─────┴─────┘

通過上述學習,你可以在 Rust 中使用 Polars 方便地進行 DataFrame 的連接和粘連。

23.2.5 基本數據類型

Polars 完全基於 Arrow 數據類型,並由 Arrow 內存數組支持。這使得數據處理緩存效率高,並且支持進程間通信。大多數數據類型完全遵循 Arrow 的實現,除了 String(實際上是 LargeUtf8)、Categorical 和 Object(支持有限)。數據類型如下:

數值類型

  • Int8:8 位有符號整數。
  • Int16:16 位有符號整數。
  • Int32:32 位有符號整數。
  • Int64:64 位有符號整數。
  • UInt8:8 位無符號整數。
  • UInt16:16 位無符號整數。
  • UInt32:32 位無符號整數。
  • UInt64:64 位無符號整數。
  • Float32:32 位浮點數。
  • Float64:64 位浮點數。

嵌套類型

  • Struct:結構體數組,表示為 Vec<Series>,用於在單列中打包多個/異質值。
  • List:列表數組,包含一個子數組和一個偏移數組(實際上是 Arrow LargeList)。

時間類型

  • Date:日期表示,內部表示為自 UNIX 紀元以來的天數,編碼為 32 位有符號整數。
  • Datetime:日期時間表示,內部表示為自 UNIX 紀元以來的微秒數,編碼為 64 位有符號整數。
  • Duration:時間間隔類型,內部表示為微秒。由 Date/Datetime 相減生成。
  • Time:時間表示,內部表示為自午夜以來的納秒數。

其他類型

  • Boolean:布爾類型,有效位打包。
  • String:字符串數據(實際上是 Arrow LargeUtf8)。
  • Binary:存儲為字節的數據。
  • Object:有限支持的數據類型,可以是任何值。
  • Categorical:字符串集合的分類編碼。
  • Enum:字符串集合的固定分類編碼。

浮點數

Polars 通常遵循 IEEE 754 浮點標準用於 Float32 和 Float64,但有一些例外:

  • 任何 NaN 與任何其他 NaN 比較時相等,並且大於任何非 NaN 值。
  • 操作不保證零或 NaN 的符號,也不保證 NaN 值的有效負載。這不僅限於算術運算,例如排序或分組操作可能將所有零規範化為 +0,將所有 NaNs 規範化為沒有負載的正 NaN,以便高效的相等性檢查。

Polars 始終嘗試提供合理準確的浮點計算結果,但除非另有說明,否則不保證誤差。通常 100% 準確的結果獲取代價高昂(需要比 64 位浮點數更大的內部表示),因此總會存在一些誤差。


示例

數值類型示例

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = df! {
    "int8_col" => &[1i8, 2, 3],
    "int16_col" => &[100i16, 200, 300],
    "int32_col" => &[1000i32, 2000, 3000],
    "float64_col" => &[1.1f64, 2.2, 3.3],
}.unwrap();

println!("{}", df);
}

嵌套類型示例

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = df! {
    "list_col" => &[vec![1, 2, 3], vec![4, 5, 6]],
}.unwrap();

println!("{}", df);
}

時間類型示例

#![allow(unused)]
fn main() {
use polars::prelude::*;
use chrono::NaiveDate;

let df = df! {
    "date_col" => &[NaiveDate::from_ymd(2021, 1, 1), NaiveDate::from_ymd(2021, 1, 2)],
}.unwrap();

println!("{}", df);
}

通過這些示例,你可以瞭解如何在 Rust 中使用 Polars 處理各種數據類型。

23.2.6 數據結構

數據結構

Polars 提供的核心數據結構是 Series 和 DataFrame。

Series

Series 是一維數據結構,其中所有元素具有相同的數據類型。以下代碼展示瞭如何創建一個簡單的 Series 對象:

#![allow(unused)]
fn main() {
use polars::prelude::*;

// 創建名為 "a" 的 Series 對象
let s = Series::new("a", &[1, 2, 3, 4, 5]);

// 打印 Series 對象
println!("{}", s);
}

輸出示例:

shape: (5,)
Series: 'a' [i64]
[
    1
    2
    3
    4
    5
]
DataFrame

DataFrame 是由 Series 支持的二維數據結構,可以看作是一系列 Series 的抽象集合。可以對 DataFrame 執行類似 SQL 查詢的操作,如 GROUP BY、JOIN、PIVOT 等,還可以定義自定義函數。

#![allow(unused)]
fn main() {
use chrono::NaiveDate;
use polars::prelude::*;

// 創建一個 DataFrame 對象
let df: DataFrame = df!(
    "integer" => &[1, 2, 3, 4, 5],
    "date" => &[
        NaiveDate::from_ymd_opt(2025, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap(),
        NaiveDate::from_ymd_opt(2025, 1, 2).unwrap().and_hms_opt(0, 0, 0).unwrap(),
        NaiveDate::from_ymd_opt(2025, 1, 3).unwrap().and_hms_opt(0, 0, 0).unwrap(),
        NaiveDate::from_ymd_opt(2025, 1, 4).unwrap().and_hms_opt(0, 0, 0).unwrap(),
        NaiveDate::from_ymd_opt(2025, 1, 5).unwrap().and_hms_opt(0, 0, 0).unwrap(),
    ],
    "float" => &[4.0, 5.0, 6.0, 7.0, 8.0]
)
.unwrap();

// 打印 DataFrame 對象
println!("{}", df);
}

輸出示例:

shape: (5, 3)
┌─────────┬─────────────────────┬───────┐
│ integer ┆ date                ┆ float │
│ ---     ┆ ---                 ┆ ---   │
│ i64     ┆ datetime[μs]        ┆ f64   │
╞═════════╪═════════════════════╪═══════╡
│ 1       ┆ 2022-01-01 00:00:00 ┆ 4.0   │
│ 2       ┆ 2022-01-02 00:00:00 ┆ 5.0   │
│ 3       ┆ 2022-01-03 00:00:00 ┆ 6.0   │
│ 4       ┆ 2022-01-04 00:00:00 ┆ 7.0   │
│ 5       ┆ 2022-01-05 00:00:00 ┆ 8.0   │
└─────────┴─────────────────────┴───────┘

查看數據

以下部分將介紹如何查看 DataFrame 中的數據。我們將使用前面的 DataFrame 作為示例。

head 函數默認顯示 DataFrame 的前 5 行。你可以指定要查看的行數(例如 df.head(10))。

#![allow(unused)]
fn main() {
let df_head = df.head(Some(3));

// 打印前 3 行數據
println!("{}", df_head);
}

輸出示例:

shape: (3, 3)
┌─────────┬─────────────────────┬───────┐
│ integer ┆ date                ┆ float │
│ ---     ┆ ---                 ┆ ---   │
│ i64     ┆ datetime[μs]        ┆ f64   │
╞═════════╪═════════════════════╪═══════╡
│ 1       ┆ 2022-01-01 00:00:00 ┆ 4.0   │
│ 2       ┆ 2022-01-02 00:00:00 ┆ 5.0   │
│ 3       ┆ 2022-01-03 00:00:00 ┆ 6.0   │
└─────────┴─────────────────────┴───────┘
Tail

tail 函數顯示 DataFrame 的最後 5 行。你也可以指定要查看的行數,類似於 head

#![allow(unused)]
fn main() {
let df_tail = df.tail(Some(3));

// 打印後 3 行數據
println!("{}", df_tail);
}

輸出示例:

shape: (3, 3)
┌─────────┬─────────────────────┬───────┐
│ integer ┆ date                ┆ float │
│ ---     ┆ ---                 ┆ ---   │
│ i64     ┆ datetime[μs]        ┆ f64   │
╞═════════╪═════════════════════╪═══════╡
│ 3       ┆ 2022-01-03 00:00:00 ┆ 6.0   │
│ 4       ┆ 2022-01-04 00:00:00 ┆ 7.0   │
│ 5       ┆ 2022-01-05 00:00:00 ┆ 8.0   │
└─────────┴─────────────────────┴───────┘
Sample

如果你想隨機查看 DataFrame 中的一些數據,你可以使用 samplesample 可以從 DataFrame 中獲取 n 行隨機行。

#![allow(unused)]
fn main() {
use polars::prelude::*;

let n = Series::new("", &[2]);
let sampled_df = df.sample_n(&n, false, false, None).unwrap();

// 打印隨機抽樣的數據
println!("{}", sampled_df);
}

輸出示例:

shape: (2, 3)
┌─────────┬─────────────────────┬───────┐
│ integer ┆ date                ┆ float │
│ ---     ┆ ---                 ┆ ---   │
│ i64     ┆ datetime[μs]        ┆ f64   │
╞═════════╪═════════════════════╪═══════╡
│ 3       ┆ 2022-01-03 00:00:00 ┆ 6.0   │
│ 2       ┆ 2022-01-02 00:00:00 ┆ 5.0   │
└─────────┴─────────────────────┴───────┘
描述(Describe)

describe 返回 DataFrame 的摘要統計信息。如果可能,它將提供一些快速統計信息。

注意,很遺憾,在 Rust 中,這個功能目前不可用。

23.3 Polars進階學習

23.3.1 聚合操作 Aggregation

聚合操作在量化金融中的應用

Polars 實現了強大的語法,既可以在惰性 API 中定義,也可以在急性 API 中定義。讓我們看一下這意味著什麼。

我們可以從一個簡單的期貨和期權交易數據集開始。

#![allow(unused)]
fn main() {
use std::io::Cursor;
use reqwest::blocking::Client;
use polars::prelude::*;

let url = "https://example.com/financial-data.csv";

let mut schema = Schema::new();
schema.with_column(
    "symbol".into(),
    DataType::Categorical(None, Default::default()),
);
schema.with_column(
    "type".into(),
    DataType::Categorical(None, Default::default()),
);
schema.with_column(
    "trade_date".into(),
    DataType::Date,
);
schema.with_column(
    "open".into(),
    DataType::Float64,
);
schema.with_column(
    "close".into(),
    DataType::Float64,
);
schema.with_column(
    "volume".into(),
    DataType::Float64,
);

let data: Vec<u8> = Client::new().get(url).send()?.text()?.bytes().collect();

let dataset = CsvReadOptions::default()
    .with_has_header(true)
    .with_schema(Some(Arc::new(schema)))
    .map_parse_options(|parse_options| parse_options.with_try_parse_dates(true))
    .into_reader_with_file_handle(Cursor::new(data))
    .finish()?;

println!("{}", &dataset);
}

基本聚合

我們可以按 symboltype 分組,並計算每組的成交量總和、開盤價和收盤價的平均值。

#![allow(unused)]
fn main() {
let df = dataset
    .clone()
    .lazy()
    .group_by(["symbol", "type"])
    .agg([
        sum("volume").alias("total_volume"),
        mean("open").alias("avg_open"),
        mean("close").alias("avg_close"),
    ])
    .sort(
        ["total_volume"],
        SortMultipleOptions::default()
            .with_order_descending(true)
            .with_nulls_last(true),
    )
    .limit(5)
    .collect()?;

println!("{}", df);
}

條件聚合

我們想知道每個交易日中漲幅超過5%的交易記錄數。可以直接在聚合中查詢:

#![allow(unused)]
fn main() {
let df = dataset
    .clone()
    .lazy()
    .group_by(["trade_date"])
    .agg([
        (col("close") - col("open")).gt(lit(0.05)).sum().alias("gains_over_5pct"),
    ])
    .sort(
        ["gains_over_5pct"],
        SortMultipleOptions::default().with_order_descending(true),
    )
    .limit(5)
    .collect()?;

println!("{}", df);
}

嵌套分組

在嵌套分組中,表達式在組內工作,因此可以生成任意長度的結果。例如,我們想按 symboltype 分組,並計算每組的交易量總和和記錄數:

#![allow(unused)]
fn main() {
let df = dataset
    .clone()
    .lazy()
    .group_by(["symbol", "type"])
    .agg([
        col("volume").sum().alias("total_volume"),
        col("symbol").count().alias("record_count"),
    ])
    .sort(
        ["total_volume"],
        SortMultipleOptions::default()
            .with_order_descending(true)
            .with_nulls_last(true),
    )
    .limit(5)
    .collect()?;

println!("{}", df);
}

過濾組內數據

我們可以計算每個交易日的平均漲幅,但不包含成交量低於 1000 的交易記錄:

#![allow(unused)]
fn main() {
fn compute_change() -> Expr {
    (col("close") - col("open")) / col("open") * lit(100)
}

fn avg_change_with_volume_filter() -> Expr {
    compute_change()
        .filter(col("volume").gt(lit(1000)))
        .mean()
        .alias("avg_change_filtered")
}

let df = dataset
    .clone()
    .lazy()
    .group_by(["trade_date"])
    .agg([
        avg_change_with_volume_filter(),
        col("volume").sum().alias("total_volume"),
    ])
    .limit(5)
    .collect()?;

println!("{}", df);
}

排序

我們可以按交易日期排序,並按 symbol 分組以獲得每個 symbol 的最高和最低收盤價:

#![allow(unused)]
fn main() {
fn get_price_range() -> Expr {
    col("close")
}

let df = dataset
    .clone()
    .lazy()
    .sort(
        ["trade_date"],
        SortMultipleOptions::default()
            .with_order_descending(true)
            .with_nulls_last(true),
    )
    .group_by(["symbol"])
    .agg([
        get_price_range().max().alias("max_close"),
        get_price_range().min().alias("min_close"),
    ])
    .limit(5)
    .collect()?;

println!("{}", df);
}

我們還可以在 group_by 上下文中按另一列排序:

#![allow(unused)]
fn main() {
let df = dataset
    .clone()
    .lazy()
    .sort(
        ["trade_date"],
        SortMultipleOptions::default()
            .with_order_descending(true)
            .with_nulls_last(true),
    )
    .group_by(["symbol"])
    .agg([
        get_price_range().max().alias("max_close"),
        get_price_range().min().alias("min_close"),
        col("type")
            .sort_by(["symbol"], SortMultipleOptions::default())
            .first()
            .alias("first_type"),
    ])
    .sort(["symbol"], SortMultipleOptions::default())
    .limit(5)
    .collect()?;

println!("{}", df);
}

23.3.2 Folds

Folds

Polars 提供了一些用於橫向聚合的表達式和方法,如 sum、min、mean 等。然而,當你需要更復雜的聚合時,Polars 默認的方法可能不夠用。這時,摺疊(fold)操作就派上用場了。

摺疊表達式在列上操作,最大限度地提高了速度。它非常高效地利用數據佈局,並且通常具有向量化執行的特點。

手動求和

我們從一個示例開始,通過摺疊實現求和操作。

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = df!(
    "price" => &[100, 200, 300],
    "quantity" => &[2, 3, 4],
)?;

let out = df
    .lazy()
    .select([fold_exprs(lit(0), |acc, x| (acc + x).map(Some), [col("*")]).alias("sum")])
    .collect()?;
println!("{}", out);

shape: (3, 1)
┌─────┐
│ sum │
│ --- │
│ i64 │
╞═════╡
│ 102 │
│ 203 │
│ 304 │
└─────┘
}

上述代碼遞歸地將函數 f(acc, x) -> acc 應用到累加器 acc 和新列 x 上。這個函數單獨在列上操作,可以利用緩存效率和向量化執行。

條件聚合

如果你想對 DataFrame 中的所有列應用條件或謂詞,摺疊操作可以非常簡潔地表達這種需求。

#![allow(unused)]
fn main() {
let df = df!(
    "price" => &[100, 200, 300],
    "quantity" => &[2, 3, 4],
)?;

let out = df
    .lazy()
    .filter(fold_exprs(
        lit(true),
        |acc, x| acc.bitand(&x).map(Some),
        [col("*").gt(150)],
    ))
    .collect()?;
println!("{}", out);

shape: (1, 2)
┌───────┬─────────┐
│ price ┆ quantity│
│ ----- ┆ ------- │
│ i64   ┆ i64     │
╞═══════╪═════════╡
│ 300   ┆ 4       │
└───────┴─────────┘
}

在上述代碼片段中,我們過濾出所有列值大於 150 的行。

摺疊和字符串數據

摺疊可以用來連接字符串數據。然而,由於中間列的物化,這種操作的複雜度會呈平方級增長。因此,我們推薦使用 concat_str 表達式來完成這類操作。

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = df!(
    "symbol" => &["AAPL", "GOOGL", "AMZN"],
    "price" => &[150, 2800, 3400],
)?;

let out = df
    .lazy()
    .select([concat_str([col("symbol"), col("price")], "", false).alias("combined")])
    .collect()?;
println!("{:?}", out);

shape: (3, 1)
┌───────────┐
│ combined  │
│ ---       │
│ str       │
╞═══════════╡
│ AAPL150   │
│ GOOGL2800 │
│ AMZN3400  │
└───────────┘
}

通過使用 concat_str 表達式,我們可以高效地連接字符串數據,避免了複雜的操作。

23.3.3 CSV input

CSV

讀取與寫入

讀取CSV文件的方式很常見:

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = CsvReadOptions::default()
    .try_into_reader_with_file_path(Some("docs/data/path.csv".into()))
    .unwrap()
    .finish()
    .unwrap();
}

在這個示例中,我們使用CsvReadOptions來設置CSV讀取選項,然後將文件路徑傳遞給try_into_reader_with_file_path方法,最終通過finish方法完成讀取並獲取DataFrame。

寫入CSV文件使用write_csv函數:

#![allow(unused)]
fn main() {
use polars::prelude::*;

let mut df = df!(
    "foo" => &[1, 2, 3],
    "bar" => &[None, Some("bak"), Some("baz")],
).unwrap();

let mut file = std::fs::File::create("docs/data/path.csv").unwrap();
CsvWriter::new(&mut file).finish(&mut df).unwrap();
}

在這個示例中,我們創建一個DataFrame並將其寫入指定路徑的CSV文件中。

掃描CSV

Polars允許你掃描CSV輸入。掃描延遲了文件的實際解析,返回一個名為LazyFrame的惰性計算持有者。

#![allow(unused)]
fn main() {
use polars::prelude::*;

let lf = LazyCsvReader::new("./test.csv").finish().unwrap();
}

使用LazyCsvReader,可以在不立即解析文件的情況下處理CSV輸入,這對優化性能有很大幫助。

教程總結

讀取CSV文件

  1. 導入Polars庫。
  2. 使用CsvReadOptions配置CSV讀取選項。
  3. 調用try_into_reader_with_file_path方法傳入文件路徑。
  4. 使用finish方法完成讀取並獲取DataFrame。

寫入CSV文件

  1. 創建一個DataFrame對象。
  2. 使用std::fs::File::create創建文件。
  3. 使用CsvWriter將DataFrame寫入CSV文件。

掃描CSV文件

  1. 使用LazyCsvReader延遲解析CSV文件。
  2. 使用finish方法獲取LazyFrame。

通過以上方法,可以高效地讀取、寫入和掃描CSV文件,極大地提升數據處理的性能和靈活性。

參考代碼示例

讀取CSV文件

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = CsvReadOptions::default()
    .try_into_reader_with_file_path(Some("docs/data/path.csv".into()))
    .unwrap()
    .finish()
    .unwrap();
println!("{}", df);
}

寫入CSV文件

#![allow(unused)]
fn main() {
use polars::prelude::*;

let mut df = df!(
    "foo" => &[1, 2, 3],
    "bar" => &[None, Some("bak"), Some("baz")],
).unwrap();

let mut file = std::fs::File::create("docs/data/path.csv").unwrap();
CsvWriter::new(&mut file).finish(&mut df).unwrap();
}

掃描CSV文件

#![allow(unused)]
fn main() {
use polars::prelude::*;

let lf = LazyCsvReader::new("./test.csv").finish().unwrap();
println!("{:?}", lf);
}

通過上述步驟,用戶可以輕鬆掌握在Rust中使用Polars處理CSV文件的基本方法。

23.3.4 JSON input

JSON 文件

Polars 可以讀取和寫入標準 JSON 和換行分隔的 JSON (NDJSON)。

讀取

標準 JSON

讀取 JSON 文件的方式如下:

#![allow(unused)]
fn main() {
use polars::prelude::*;
let mut file = std::fs::File::open("docs/data/path.json").unwrap();
let df = JsonReader::new(&mut file).finish().unwrap();
}
換行分隔的 JSON

Polars 可以更高效地讀取 NDJSON 文件:

#![allow(unused)]
fn main() {
use polars::prelude::*;
let mut file = std::fs::File::open("docs/data/path.json").unwrap();
let df = JsonLineReader::new(&mut file).finish().unwrap();
}

寫入

將 DataFrame 寫入 JSON 文件:

#![allow(unused)]
fn main() {
use polars::prelude::*;
let mut df = df!(
    "foo" => &[1, 2, 3],
    "bar" => &[None, Some("bak"), Some("baz")],
).unwrap();
let mut file = std::fs::File::create("docs/data/path.json").unwrap();

// 寫入標準 JSON
JsonWriter::new(&mut file)
    .with_json_format(JsonFormat::Json)
    .finish(&mut df)
    .unwrap();

// 寫入 NDJSON
JsonWriter::new(&mut file)
    .with_json_format(JsonFormat::JsonLines)
    .finish(&mut df)
    .unwrap();
}

掃描

Polars 允許僅掃描換行分隔的 JSON 輸入。掃描延遲了文件的實際解析,返回一個名為 LazyFrame 的惰性計算持有者。

#![allow(unused)]
fn main() {
use polars::prelude::*;
let lf = LazyJsonLineReader::new("docs/data/path.json")
    .finish()
    .unwrap();
}

23.3.5 Polars的急性和惰性模式 (Lazy / Eager API)

Polars 提供了兩種操作模式:急性(Eager)和惰性(Lazy)。急性模式下,查詢會立即執行,而惰性模式下,查詢會在“需要”時才評估。推遲執行可以顯著提升性能,因此在大多數情況下優先使用惰性 API。下面通過一個例子進行說明:

急性模式示例

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = CsvReadOptions::default()
    .try_into_reader_with_file_path(Some("docs/data/iris.csv".into()))
    .unwrap()
    .finish()
    .unwrap();
let mask = df.column("sepal_length")?.f64()?.gt(5.0);
let df_small = df.filter(&mask)?;
#[allow(deprecated)]
let df_agg = df_small
    .group_by(["species"])?
    .select(["sepal_width"])
    .mean()?;
println!("{}", df_agg);
}

在這個例子中,我們使用急性 API:

  1. 讀取鳶尾花數據集。
  2. 根據萼片長度過濾數據集。
  3. 計算每個物種的萼片寬度平均值。

每一步都立即執行並返回中間結果。這可能會浪費資源,因為我們可能會執行不必要的工作或加載未使用的數據。

惰性模式示例

#![allow(unused)]
fn main() {
use polars::prelude::*;

let q = LazyCsvReader::new("docs/data/iris.csv")
    .with_has_header(true)
    .finish()?
    .filter(col("sepal_length").gt(lit(5)))
    .group_by(vec![col("species")])
    .agg([col("sepal_width").mean()]);
let df = q.collect()?;
println!("{}", df);
}

在這個例子中,使用惰性 API 可以進行以下優化:

  1. 謂詞下推(Predicate pushdown):在讀取數據集時儘早應用過濾器,僅讀取萼片長度大於 5 的行。
  2. 投影下推(Projection pushdown):在讀取數據集時只選擇所需的列,從而不需要加載額外的列(如花瓣長度和花瓣寬度)。

這些優化顯著降低了內存和 CPU 的負載,從而允許在內存中處理更大的數據集並加快處理速度。一旦定義了查詢,通過調用 collect 來執行它。在 Lazy API 章節中,我們將詳細討論其實現。

急性 API

在很多情況下,急性 API 實際上是在底層調用惰性 API,並立即收集結果。這具有在查詢內部仍然可以進行查詢計劃優化的好處。

何時使用哪種模式

通常應優先使用惰性 API,除非您對中間結果感興趣或正在進行探索性工作,並且尚不確定查詢的最終形態。

量化金融案例

急性模式示例:計算股票的簡單移動平均線(SMA)

#![allow(unused)]
fn main() {
use polars::prelude::*;
use chrono::NaiveDate;

let df = df!(
    "date" => &[
        NaiveDate::from_ymd(2023, 1, 1),
        NaiveDate::from_ymd(2023, 1, 2),
        NaiveDate::from_ymd(2023, 1, 3),
        NaiveDate::from_ymd(2023, 1, 4),
        NaiveDate::from_ymd(2023, 1, 5),
    ],
    "price" => &[100.0, 101.0, 102.0, 103.0, 104.0],
)?;

let sma = df
    .clone()
    .select([col("price").rolling_mean(3, None, false, false).alias("SMA")])
    .collect()?;

println!("{}", sma);
}

惰性模式示例:計算股票的加權移動平均線(WMA)

#![allow(unused)]
fn main() {
let df = df!(
    "date" => &[
        NaiveDate::from_ymd(2023, 1, 1),
        NaiveDate::from_ymd(2023, 1, 2),
        NaiveDate::from_ymd(2023, 1, 3),
        NaiveDate::from_ymd(2023, 1, 4),
        NaiveDate::from_ymd(2023, 1, 5),
    ],
    "price" => &[100.0, 101.0, 102.0, 103.0, 104.0],
)?;

let weights = vec![0.5, 0.3, 0.2];

let wma = df
    .lazy()
    .with_column(
        col("price")
            .rolling_apply(
                |s| {
                    let weighted_sum: f64 = s
                        .f64()
                        .unwrap()
                        .into_iter()
                        .zip(&weights)
                        .map(|(x, &w)| x.unwrap() * w)
                        .sum();
                    Some(weighted_sum)
                },
                3,
                polars::prelude::RollingOptions::default()
                    .min_periods(1)
                    .center(false)
                    .window_size(3)
            )
            .alias("WMA")
    )
    .collect()?;

println!("{}", wma);
}

23.3.6 流模式 (Streaming Mode)

Polars 引入了一個強大的功能叫做流模式(Streaming Mode),設計用於通過分塊處理數據來高效處理大型數據集。該模式顯著提高了數據處理任務的性能,特別是在處理無法全部裝入內存的海量數據集時。

流模式的關鍵特性:

  1. 基於塊的處理:Polars 以塊的形式處理數據,減少內存使用,使其能夠高效處理大型數據集。
  2. 自動優化:流模式包含諸如謂詞下推(predicate pushdown)和投影下推(projection pushdown)等優化,以最小化處理和讀取的數據量。
  3. 並行執行:Polars 利用所有可用的 CPU 核心,通過劃分工作負載來加快數據處理速度。

量化金融案例

考慮一個需要處理大型股票交易數據集的場景。使用 Polars 流模式,我們可以高效地從包含數百萬交易記錄的 CSV 文件中計算每個股票代碼的平均交易價格。

急性 API 示例

使用急性 API 時,操作會立即執行:

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = CsvReadOptions::default()
    .try_into_reader_with_file_path(Some("docs/data/stock_trades.csv".into()))
    .unwrap()
    .finish()
    .unwrap();

let mask = df.column("trade_price")?.f64()?.gt(100.0);
let df_filtered = df.filter(&mask)?;
#[allow(deprecated)]
let df_agg = df_filtered
    .group_by(["stock_symbol"])?
    .select(["trade_price"])
    .mean()?;
println!("{}", df_agg);
}

在這個示例中:

  1. 讀取數據集。
  2. 基於交易價格過濾數據集。
  3. 計算每個股票代碼的平均交易價格。
惰性 API 示例(帶流模式)

使用惰性 API 並啟用流模式可以延遲執行和優化:

#![allow(unused)]
fn main() {
use polars::prelude::*;

let q = LazyCsvReader::new("docs/data/stock_trades.csv")
    .with_has_header(true)
    .finish()?
    .filter(col("trade_price").gt(lit(100)))
    .group_by(vec![col("stock_symbol")])
    .agg([col("trade_price").mean()]);
let df = q.collect()?;
println!("{}", df);
}

在這個示例中:

  1. 定義查詢但不立即執行。
  2. 查詢計劃器在數據掃描期間應用優化,如過濾和選擇列。
  3. 查詢以塊的形式執行,減少內存使用並提高性能。

配置塊大小

默認塊大小由列數和可用線程數決定,但可以手動設置以進一步優化性能:

#![allow(unused)]
fn main() {
use polars::prelude::*;

pl::Config::set_streaming_chunk_size(50000);

let q = LazyCsvReader::new("docs/data/stock_trades.csv")
    .with_has_header(true)
    .finish()?
    .filter(col("trade_price").gt(lit(100)))
    .group_by(vec![col("stock_symbol")])
    .agg([col("trade_price").mean()]);
let df = q.collect()?;
println!("{}", df);
}

設置塊大小有助於根據具體需求和硬件能力平衡內存使用和處理速度。

流模式的優勢

  1. 內存效率:通過分塊處理數據,顯著減少內存使用。
  2. 速度:並行執行和查詢優化加快了數據處理速度。
  3. 可擴展性:通過從磁盤中分塊流式傳輸數據,處理超過內存限制的大型數據集。

Polars 流模式在量化金融中尤其有用,因為大量數據集很常見,高效的數據處理對於及時分析和決策至關重要。

使用流模式執行查詢

Polars 支持通過傳遞 streaming=True 參數到 collect 方法,以流方式執行查詢。

#![allow(unused)]
fn main() {
use polars::prelude::*;

let q1 = LazyCsvReader::new("docs/data/iris.csv")
    .with_has_header(true)
    .finish()?
    .filter(col("sepal_length").gt(lit(5)))
    .group_by(vec![col("species")])
    .agg([col("sepal_width").mean()]);

let df = q1.clone().with_streaming(true).collect()?;
println!("{}", df);
}

何時可用流模式?

流模式仍在開發中。我們可以請求 Polars 以流模式執行任何惰性查詢,但並非所有惰性操作都支持流模式。如果某個操作不支持流模式,Polars 將在非流模式下運行查詢。

流模式支持許多操作,包括:

  • 過濾、切片、頭、尾
  • with_columns、select
  • group_by
  • 連接
  • 唯一
  • 排序
  • 爆炸、反透視
  • scan_csv、scan_parquet、scan_ipc

這個列表並不詳盡。Polars 正在積極開發中,更多操作可能會在沒有明確通知的情況下添加。

示例(帶支持操作)

要確定查詢的哪些部分是流式的,可以使用 explain 方法。以下是一個演示如何檢查查詢計劃的示例:

#![allow(unused)]
fn main() {
use polars::prelude::*;

let query_plan = q1.with_streaming(true).explain(true)?;
println!("{}", query_plan);

STREAMING:
  AGGREGATE
    [col("sepal_width").mean()] BY [col("species")] FROM
    Csv SCAN [docs/data/iris.csv]
    PROJECT 3/5 COLUMNS
    SELECTION: [(col("sepal_length")) > (5.0)]
}

示例(帶非流式操作)

#![allow(unused)]
fn main() {
use polars::prelude::*;

let q2 = LazyCsvReader::new("docs/data/iris.csv")
    .finish()?
    .with_columns(vec![col("sepal_length")
        .mean()
        .over(vec![col("species")])
        .alias("sepal_length_mean")]);

let query_plan = q2.with_streaming(true).explain(true)?;
println!("{}", query_plan);

WITH_COLUMNS:
[col("sepal_length").mean().over([col("species")])] 
STREAMING:
Csv SCAN [docs/data/iris.csv]
PROJECT */5 COLUMNS
}

23.3.7 缺失值處理 Missihg Values

本頁面介紹了在 Polars 中如何表示缺失數據以及如何填充缺失數據。

null 和 NaN 值

在 Polars 中,每個 DataFrame(或 Series)中的列都是一個 Arrow 數組或基於 Apache Arrow 規範的 Arrow 數組集合。缺失數據在 Arrow 和 Polars 中用 null 值表示。這種 null 缺失值適用於所有數據類型,包括數值型數據。

此外,Polars 還允許在浮點數列中使用 NaN(非數值)值。NaN 值被視為浮點數據類型的一部分,而不是缺失數據。我們將在下面單獨討論 NaN 值。

可以使用 Rust 中的 None 值手動定義缺失值:

#![allow(unused)]
fn main() {
use polars::prelude::*;

let df = df!(
    "value" => &[Some(1), None],
)?;

println!("{}", &df);

shape: (2, 1)
┌───────┐
│ value │
│ ---   │
│ i64   │
╞═══════╡
│ 1     │
│ null  │
└───────┘
}

缺失數據元數據

每個由 Polars 使用的 Arrow 數組都存儲了與缺失數據相關的兩種元數據。這些元數據允許 Polars 快速顯示有多少缺失值以及哪些值是缺失的。

第一種元數據是 null_count,即列中 null 值的行數:

#![allow(unused)]
fn main() {
let null_count_df = df.null_count();
println!("{}", &null_count_df);

shape: (1, 1)
┌───────┐
│ value │
│ ---   │
│ u32   │
╞═══════╡
│ 1     │
└───────┘
}

第二種元數據是一個叫做有效性位圖(validity bitmap)的數組,指示每個數據值是有效的還是缺失的。有效性位圖在內存中是高效的,因為它是按位編碼的 - 每個值要麼是 0 要麼是 1。這種按位編碼意味著每個數組的內存開銷僅為(數組長度 / 8)字節。有效性位圖由 Polars 的 is_null 方法使用。

可以使用 is_null 方法返回基於有效性位圖的 Series:

#![allow(unused)]
fn main() {
let is_null_series = df
    .clone()
    .lazy()
    .select([col("value").is_null()])
    .collect()?;
println!("{}", &is_null_series);

shape: (2, 1)
┌───────┐
│ value │
│ ---   │
│ bool  │
╞═══════╡
│ false │
│ true  │
└───────┘
}

填充缺失數據

可以使用 fill_null 方法填充 Series 中的缺失數據。您需要指定希望 fill_null 方法如何填充缺失數據。主要有以下幾種方式:

  1. 使用字面值,例如 0 或 "0"
  2. 使用策略,例如前向填充
  3. 使用表達式,例如用另一列的值替換
  4. 插值

我們通過定義一個簡單的 DataFrame,其中 col2 有一個缺失值,來說明每種填充缺失值的方法:

#![allow(unused)]
fn main() {
let df = df!(
    "col1" => &[Some(1), Some(2), Some(3)],
    "col2" => &[Some(1), None, Some(3)],
)?;
println!("{}", &df);

shape: (3, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ ---  ┆ ---  │
│ i64  ┆ i64  │
╞══════╪══════╡
│ 1    ┆ 1    │
│ 2    ┆ null │
│ 3    ┆ 3    │
└──────┴──────┘
}

使用指定字面值填充

我們可以用一個指定的字面值填充缺失數據:

#![allow(unused)]
fn main() {
let fill_literal_df = df
    .clone()
    .lazy()
    .with_columns([col("col2").fill_null(lit(2))])
    .collect()?;
println!("{}", &fill_literal_df);

shape: (3, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ ---  ┆ ---  │
│ i64  ┆ i64  │
╞══════╪══════╡
│ 1    ┆ 1    │
│ 2    ┆ 2    │
│ 3    ┆ 3    │
└──────┴──────┘
}

使用策略填充

我們可以用一種策略來填充缺失數據,例如前向填充:

#![allow(unused)]
fn main() {
let fill_forward_df = df
    .clone()
    .lazy()
    .with_columns([col("col2").forward_fill(None)])
    .collect()?;
println!("{}", &fill_forward_df);

shape: (3, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ ---  ┆ ---  │
│ i64  ┆ i64  │
╞══════╪══════╡
│ 1    ┆ 1    │
│ 2    ┆ 1    │
│ 3    ┆ 3    │
└──────┴──────┘
}

使用表達式填充

為了更靈活地填充缺失數據,我們可以使用表達式。例如,用該列的中位數填充 null 值:

#![allow(unused)]
fn main() {
let fill_median_df = df
    .clone()
    .lazy()
    .with_columns([col("col2").fill_null(median("col2"))])
    .collect()?;
println!("{}", &fill_median_df);

shape: (3, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ ---  ┆ ---  │
│ i64  ┆ f64  │
╞══════╪══════╡
│ 1    ┆ 1.0  │
│ 2    ┆ 2.0  │
│ 3    ┆ 3.0  │
└──────┴──────┘
}

在這種情況下,由於中位數是浮點數統計數據,列從整數類型轉換為浮點類型。

使用插值填充

此外,我們可以使用插值(不使用 fill_null 函數)來填充 null 值:

#![allow(unused)]
fn main() {
let fill_interpolation_df = df
    .clone()
    .lazy()
    .with_columns([col("col2").interpolate(InterpolationMethod::Linear)])
    .collect()?;
println!("{}", &fill_interpolation_df);

shape: (3, 2)
┌──────┬──────┐
│ col1 ┆ col2 │
│ ---  ┆ ---  │
│ i64  ┆ f64  │
╞══════╪══════╡
│ 1    ┆ 1.0  │
│ 2    ┆ 2.0  │
│ 3    ┆ 3.0  │
└──────┴──────┘
}

NaN 值

Series 中的缺失數據有一個 null 值。然而,您可以在浮點數數據類型的列中使用 NaN 值。這些 NaN 值可以由 Numpy 的 np.nan 或原生的 float('nan') 創建:

#![allow(unused)]
fn main() {
let nan_df = df!(
    "value" => [1.0, f64::NAN, f64::NAN, 3.0],
)?;
println!("{}", &nan_df);

shape: (4, 1)
┌───────┐
│ value │
│ ---   │
│ f64   │
╞═══════╡
│ 1.0   │
│ NaN   │
│ NaN   │
│ 3.0   │
└───────┘
}

NaN 值被視為浮點數據類型的一部分,而不是缺失數據。這意味著:

  • NaN 值不會被 null_count 方法計數。
  • 當您使用 fill_nan 方法時,NaN 值會被填充,但不會被 fill_null 方法填充。

Polars 具有 is_nanfill_nan 方法,類似於 is_nullfill_null 方法。基礎 Arrow 數組沒有預先計算的 NaN 值有效位圖,因此 is_nan 方法必須計算這個位圖。

null 和 NaN 值之間的另一個區別是,計算包含 null 值的列的平均值時,會排除 null 值,而包含 NaN 值的列的平均值結果為 NaN。這種行為可以通過用 null 值替換 NaN 值來避免:

#![allow(unused)]
fn main() {
let mean_nan_df = nan_df
   
}

23.3.8 窗口函數 Window functions

窗口函數

窗口函數是帶有超級功能的表達式。它們允許您在選擇上下文中對分組進行聚合。首先,我們創建一個數據集。在下面的代碼片段中加載的數據集包含一些關於金融股票的信息:

數據集示例

#![allow(unused)]
fn main() {
use polars::prelude::*;
use reqwest::blocking::Client;

let data: Vec<u8> = Client::new()
    .get("https://example.com/financial_data.csv")  // 替換為實際的金融數據鏈接
    .send()?
    .text()?
    .bytes()
    .collect();

let file = std::io::Cursor::new(data);
let df = CsvReadOptions::default()
    .with_has_header(true)
    .into_reader_with_file_handle(file)
    .finish()?;

println!("{}", df);
}

在選擇上下文中的分組聚合

下面展示瞭如何使用窗口函數在不同的列上進行分組並對它們進行聚合。這使得我們可以在單個查詢中使用多個並行的分組操作。聚合的結果會投影回原始行,因此窗口函數幾乎總是會導致一個與原始大小相同的 DataFrame。

#![allow(unused)]
fn main() {
let out = df
    .clone()
    .lazy()
    .select([
        col("sector"),
        col("market_cap"),
        col("price")
            .mean()
            .over(["sector"])
            .alias("avg_price_by_sector"),
        col("volume")
            .mean()
            .over(["sector", "market_cap"])
            .alias("avg_volume_by_sector_and_market_cap"),
        col("price").mean().alias("avg_price"),
    ])
    .collect()?;

println!("{}", out);
}

每個分組內的操作

窗口函數不僅可以用於聚合,還可以在分組內執行其他操作。例如,如果您想對分組內的值進行排序,可以使用 col("value").sort().over("group")

#![allow(unused)]
fn main() {
let filtered = df
    .clone()
    .lazy()
    .filter(col("market_cap").gt(lit(1000000000)))  // 過濾市值大於 10 億的公司
    .select([col("company"), col("sector"), col("price")])
    .collect()?;

println!("{}", filtered);

let out = filtered
    .lazy()
    .with_columns([cols(["company", "price"])
        .sort_by(
            ["price"],
            SortMultipleOptions::default().with_order_descending(true),
        )
        .over(["sector"])])
    .collect()?;
println!("{}", out);
}

Polars 會跟蹤每個分組的位置,並將表達式映射到正確的行位置。這也適用於在單個選擇中對不同分組的操作。

窗口表達式規則

假設我們將其應用於 pl.Int32 列,窗口表達式的評估如下:

#![allow(unused)]
fn main() {
// 在分組內聚合並廣播
let _ = sum("price").over([col("sector")]);
// 在分組內求和並與分組元素相乘
let _ = (col("volume").sum() * col("price"))
    .over([col("sector")])
    .alias("volume_price_sum");
// 在分組內求和並與分組元素相乘並將分組聚合為列表
let _ = (col("volume").sum() * col("price"))
    .over([col("sector")])
    .alias("volume_price_list")
    .flatten();
}

更多示例

下面是一些窗口函數的練習示例:

  1. 按行業對所有公司進行排序。
  2. 選擇每個行業中的前三家公司作為 "top_3_in_sector"。
  3. 按價格對公司進行降序排序,並選擇每個行業中的前三家公司作為 "top_3_by_price"。
  4. 按市值對公司進行降序排序,並選擇每個行業中的前三家公司作為 "top_3_by_market_cap"。
#![allow(unused)]
fn main() {
let out = df
    .clone()
    .lazy()
    .select([
        col("sector").head(Some(3)).over(["sector"]).flatten(),
        col("company")
            .sort_by(
                ["price"],
                SortMultipleOptions::default().with_order_descending(true),
            )
            .head(Some(3))
            .over(["sector"])
            .flatten()
            .alias("top_3_by_price"),
        col("company")
            .sort_by(
                ["market_cap"],
                SortMultipleOptions::default().with_order_descending(true),
            )
            .head(Some(3))
            .over(["sector"])
            .flatten()
            .alias("top_3_by_market_cap"),
    ])
    .collect()?;
println!("{:?}", out);
}

在量化金融中,這些窗口函數可以幫助我們對股票數據進行復雜的分析和聚合操作,例如計算行業內的平均價格,篩選出每個行業中價格最高的公司等。通過這些功能,我們可以更高效地處理和分析大量的金融數據。

案例:序列化 & 轉化為polars的dataframe

為了簡單說明序列化和反序列化在polars中的作用,我寫了這段MWE代碼以演示瞭如何定義一個包含歷史股票數據的結構體,將數據序列化為 JSON 字符串,然後使用 Polars 庫創建一個數據框架並打印出來。這對於介紹如何處理金融數據以及使用 Rust 進行數據分析非常有用。

// 引入所需的庫
use serde::{Serialize, Deserialize}; // 用於序列化和反序列化
use serde_json; // 用於處理 JSON 數據
use polars::prelude::*; // 使用 Polars 處理數據
use std::io::Cursor; // 用於創建內存中的數據流

// 定義一個結構體,表示中國A股的歷史股票數據
#[derive(Debug, Serialize, Deserialize)]
struct StockZhAHist {
    date: String,         // 日期
    open: f64,            // 開盤價
    close: f64,           // 收盤價
    high: f64,            // 最高價
    low: f64,             // 最低價
    volume: f64,          // 交易量
    turnover: f64,        // 成交額
    amplitude: f64,       // 振幅
    change_rate: f64,     // 漲跌幅
    change_amount: f64,   // 漲跌額
    turnover_rate: f64,   // 換手率
}

fn main() {
    // 創建一個包含歷史股票數據的向量
    let data = vec![
        StockZhAHist { date: "1996-12-16T00:00:00.000".to_string(), open: 16.86, close: 16.86, high: 16.86, low: 16.86, volume: 62442.0, turnover: 105277000.0, amplitude: 0.0, change_rate: -10.22, change_amount: -1.92, turnover_rate: 0.87 },
        StockZhAHist { date: "1996-12-17T00:00:00.000".to_string(), open: 15.17, close: 15.17, high: 16.79, low: 15.17, volume: 463675.0, turnover: 718902016.0, amplitude: 9.61, change_rate: -10.02, change_amount: -1.69, turnover_rate: 6.49 },
        StockZhAHist { date: "1996-12-18T00:00:00.000".to_string(), open: 15.23, close: 16.69, high: 16.69, low: 15.18, volume: 445380.0, turnover: 719400000.0, amplitude: 9.95, change_rate: 10.02, change_amount: 1.52, turnover_rate: 6.24 },
        StockZhAHist { date: "1996-12-19T00:00:00.000".to_string(), open: 17.01, close: 16.4, high: 17.9, low: 15.99, volume: 572946.0, turnover: 970124992.0, amplitude: 11.44, change_rate: -1.74, change_amount: -0.29, turnover_rate: 8.03 }
    ];

    // 將歷史股票數據序列化為 JSON 字符串並打印出來
    let json = serde_json::to_string(&data).unwrap();
    println!("{}", json);

    // 從 JSON 字符串創建 Polars 數據框架
    let df = JsonReader::new(Cursor::new(json))
        .finish().unwrap();

    // 打印 Polars 數據框架
    println!("{:#?}", df);
}

返回的 Polars Dataframe表格:

dateopenclosehighamplitudechange_ratechange_amountturnover_rate
strf64f64f64f64f64f64f64
1996-12-16T0016.8616.8616.860.0-10.22-1.920.87
0:00:00.000
1996-12-17T0015.1715.1716.799.61-10.02-1.696.49
0:00:00.000
1996-12-18T0015.2316.6916.699.9510.021.526.24
0:00:00.000
1996-12-19T0017.0116.417.911.44-1.74-0.298.03
0:00:00.000

Chapter 24 - 時序數據庫Clickhouse

ClickHouse 是一個開源的列式時序數據庫管理系統(DBMS),專為高性能和低延遲的數據分析而設計。它最初由俄羅斯的互聯網公司 Yandex 開發,用於處理海量的數據分析工作負載。以下是 ClickHouse 的主要特點和介紹:

  1. 列式存儲:ClickHouse 採用列式存儲,這意味著它將數據按列存儲在磁盤上,而不是按行存儲。這種存儲方式對於數據分析非常高效,因為它允許查詢只讀取所需的列,而不必讀取整個行。這導致了更快的查詢性能和更小的存儲空間佔用。

  2. 分佈式架構:ClickHouse 具有分佈式架構,可以輕鬆擴展以處理大規模數據集。它支持數據分片、分佈式複製和負載均衡,以確保高可用性和容錯性。

  3. 支持 SQL 查詢:ClickHouse 支持標準的 SQL 查詢語言,使用戶可以使用熟悉的查詢語法執行數據分析操作。它還支持複雜的查詢,如聚合、窗口函數和子查詢。

  4. 高性能:ClickHouse 以查詢性能和吞吐量為重點進行了優化。它專為快速的數據分析查詢而設計,可以在毫秒級別內處理數十億行的數據。

  5. 實時數據注入:ClickHouse 支持實時數據注入,允許將新數據迅速插入到表中,並能夠在不停機的情況下進行數據更新。

  6. 支持多種數據格式:ClickHouse 可以處理多種數據格式,包括 JSON、CSV、Parquet 等,使其能夠與各種數據源無縫集成。

  7. 可擴展性:ClickHouse 具有可擴展性,可以與其他工具和框架(如 Apache Kafka、Spark、Presto)集成,以滿足各種數據處理需求。

  8. 開源和活躍的社區:ClickHouse 是一個開源項目,擁有活躍的社區支持。這意味著你可以免費獲取並使用它,並且有一個龐大的開發者社區,提供了大量的文檔和資源。

ClickHouse 在大數據分析、日誌處理、事件追蹤、時序數據分析等場景中得到了廣泛的應用。它的高性能、可擴展性和強大的查詢功能使其成為處理大規模數據的理想選擇。如果你需要處理大量時序數據並進行快速數據分析,那麼 ClickHouse 可能是一個非常有價值的數據庫管理系統。

24.1 安裝和配置ClickHouse數據庫

24.1.1 安裝

在Ubuntu上安裝ClickHouse:

  1. 打開終端並更新包列表:

    sudo apt update
    
  2. 安裝ClickHouse的APT存儲庫:

    sudo apt install apt-transport-https ca-certificates dirmngr
    sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E0C56BD4
    echo "deb https://repo.clickhouse.tech/deb/stable/ main/" | sudo tee /etc/apt/sources.list.d/clickhouse.list
    
  3. 再次更新包列表以獲取ClickHouse包:

    sudo apt update
    
  4. 安裝ClickHouse Server:

    sudo apt install clickhouse-server
    
  5. 啟動ClickHouse服務:

    sudo service clickhouse-server start
    
  6. 我們可以使用以下命令檢查ClickHouse服務器的狀態:

    sudo service clickhouse-server status
    

在Manjaro / Arch Linux上安裝ClickHouse:

  1. 打開終端並使用以下命令安裝ClickHouse:

    sudo pacman -S clickhouse
    
  2. 啟動ClickHouse服務:

    sudo systemctl start clickhouse-server
    
  3. 我們可以使用以下命令檢查ClickHouse服務器的狀態:

    sudo systemctl status clickhouse-server
    

這樣ClickHouse就已經安裝在你的Ubuntu或Arch Linux系統上了,並且服務已啟動。

此時如果我們如果訪問本地host上的這個網址:http://localhost:8123 ,會看到服務器返回了一個'Ok'給我們。

24.1.2 配置clickhouse的密碼

還是不要忘記,生產環境中安全是至關重要的,在ClickHouse中配置密碼需要完成以下步驟:

  1. 創建用戶和設置密碼: 首先,我們需要登錄到ClickHouse服務器上,並使用管理員權限創建用戶並設置密碼。我們可以使用ClickHouse客戶端或者通過在配置文件中執行SQL來完成這一步驟。

    使用ClickHouse客戶端:

    CREATE USER 'your_username' IDENTIFIED BY 'your_password';
    

    請將 'your_username' 替換為我們要創建的用戶名,將 'your_password' 替換為用戶的密碼。

  2. 分配權限: 創建用戶後,需要分配相應的權限。通常,我們可以使用GRANT語句來為用戶分配權限。以下是一個示例,將允許用戶對特定表執行SELECT操作:

    GRANT SELECT ON database_name.table_name TO 'your_username';
    

    這將授予 'your_username' 用戶對 'database_name.table_name' 表的SELECT權限。我們可以根據需要為用戶分配不同的權限。

  3. 配置ClickHouse服務: 接下來,我們需要配置ClickHouse服務器以啟用身份驗證。在ClickHouse的配置文件中,找到並編輯users.xml文件。通常,該文件的位置是/etc/clickhouse-server/users.xml。在該文件中,我們可以為剛剛創建的用戶添加相應的配置。

    <yandex>
        <profiles>
            <!-- 添加用戶配置 -->
            <your_username>
                <password>your_password</password>
                <networks>
                    <ip>::/0</ip> <!-- 允許所有IP連接 -->
                </networks>
            </your_username>
        </profiles>
    </yandex>
    

    請注意,這只是一個示例配置,我們需要將 'your_username''your_password' 替換為實際的用戶名和密碼。此外,上述配置允許來自所有IP地址的連接,這可能不是最安全的配置。我們可以根據需要限制連接的IP地址範圍。

  4. 重啟ClickHouse服務: 最後,重新啟動ClickHouse服務器以使配置更改生效:

    sudo systemctl restart clickhouse-server
    

    這會重新加載配置文件並應用新的用戶和權限設置。

完成上述步驟後,我們的ClickHouse服務器將配置了用戶名和密碼的身份驗證機制,並且只有具有正確憑據的用戶才能訪問相應的數據庫和表。請確保密碼強度足夠,以增強安全性。

24.2 ClickHouse for Rust: clickhouse.rs庫

clickhouse.rs 是一個網友 Paul Loyd 開發的比較成熟的第三方 Rust 庫,旨在與 ClickHouse 數據庫進行交互,提供了便捷的查詢執行、數據處理和連接管理功能。以下是該庫的一些主要特點:

主要特點

  1. 異步支持clickhouse.rs 利用了 Rust 的異步編程能力,非常適合需要非阻塞數據庫操作的高性能應用程序。
  2. 類型化接口:該庫提供了強類型接口,使數據庫交互更加安全和可預測,減少運行時錯誤並提高代碼的健壯性。
  3. 支持 ClickHouse 特性:庫支持多種 ClickHouse 特性,包括批量插入、複雜查詢和不同的數據類型。
  4. 連接池clickhouse.rs 包含連接池功能,在高負載場景下實現高效的數據庫連接管理。
  5. 易用性:該庫設計簡潔明瞭的 API,使各級 Rust 開發者都能輕鬆上手。

安裝

要使用 clickhouse.rs,需要在 Cargo.toml 中添加依賴項:

[dependencies]
clickhouse = "0.11"  # 請確保使用最新版本

基本用法

以下是使用 clickhouse.rs 連接 ClickHouse 數據庫並執行查詢的簡單示例:

use clickhouse::{Client, Row};
use futures::stream::StreamExt;
use tokio;

#[tokio::main]
async fn main() {
    // 初始化客戶端
    let client = Client::default()
        .with_url("http://localhost:8123")
        .with_database("default");

    // 執行查詢示例
    let mut cursor = client.query("SELECT number FROM system.numbers LIMIT 10").fetch().unwrap();
    while let Some(row) = cursor.next().await {
        let number: u64 = row.unwrap().get("number").unwrap();
        println!("{}", number);
    }
}

錯誤處理

該庫使用標準的 Rust 錯誤處理機制,使得管理潛在問題變得簡單。以下是處理查詢執行錯誤的示例:

use clickhouse::{Client, Error};
use tokio;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = Client::default().with_url("http://localhost:8123");

    let result = client.query("SELECT number FROM system.numbers LIMIT 10")
        .fetch()
        .await;

    match result {
        Ok(mut cursor) => {
            while let Some(row) = cursor.next().await {
                let number: u64 = row.unwrap().get("number").unwrap();
                println!("{}", number);
            }
        }
        Err(e) => eprintln!("查詢執行錯誤: {:?}", e),
    }

    Ok(())
}

高級用法

對於批量插入或處理特定數據類型等複雜場景,請參閱庫的文檔和示例。該庫支持多種 ClickHouse 特性,可以適應各種使用場景。

24.3 備份 ClickHouse 數據庫的教程

備份 ClickHouse 數據庫對於數據安全和業務連續性至關重要。通過定期備份,可以在數據丟失、硬件故障或人為錯誤時快速恢復,確保數據完整性和可用性。此外,備份有助於在系統升級或遷移過程中保護數據,避免意外損失。備份還支持增量備份和壓縮,優化存儲空間和備份速度,為企業提供靈活、高效的數據管理解決方案。通過良好的備份策略,可以大大降低數據丟失風險,保障業務穩定運行。

配置備份目的地

首先,在 /etc/clickhouse-server/config.d/backup_disk.xml 中添加備份目的地配置:

<clickhouse>
    <storage_configuration>
        <disks>
            <backups>
                <type>local</type>
                <path>/backups/</path>
            </backups>
        </disks>
    </storage_configuration>
    <backups>
        <allowed_disk>backups</allowed_disk>
        <allowed_path>/backups/</allowed_path>
    </backups>
</clickhouse>

備份整個數據庫

執行以下命令將整個數據庫備份到指定磁盤:

BACKUP DATABASE my_database TO Disk('backups', 'database_backup.zip');

恢復整個數據庫

從備份文件中恢復整個數據庫:

RESTORE DATABASE my_database FROM Disk('backups', 'database_backup.zip');

備份表

執行以下命令將表備份到指定磁盤:

BACKUP TABLE my_database.my_table TO Disk('backups', 'table_backup.zip');

恢復表

從備份文件中恢復表:

RESTORE TABLE my_database.my_table FROM Disk('backups', 'table_backup.zip');

增量備份

指定基礎備份進行增量備份:

BACKUP DATABASE my_database TO Disk('backups', 'incremental_backup.zip') SETTINGS base_backup = Disk('backups', 'database_backup.zip');

使用密碼保護備份

備份文件使用密碼保護:

BACKUP DATABASE my_database TO Disk('backups', 'protected_backup.zip') SETTINGS password='yourpassword';

壓縮設置

指定壓縮方法和級別:

BACKUP DATABASE my_database TO Disk('backups', 'compressed_backup.zip') SETTINGS compression_method='lzma', compression_level=3;

恢復特定分區

從備份中恢復特定分區:

RESTORE TABLE my_database.my_table PARTITIONS 'partition_id' FROM Disk('backups', 'table_backup.zip');

更多詳細信息請參考 ClickHouse 文檔

24.4 關於Clickhouse的優化

在量化金融領域,處理大量實時數據至關重要。ClickHouse作為一款高性能列式數據庫,提供了高效的查詢和存儲方案。然而,為了充分發揮其性能,必須對其進行優化。

硬件優化

  1. 存儲設備:選擇高性能 SSD,可以顯著提高數據讀取和寫入速度。
  2. 內存:增加內存容量,有助於更快地處理大量數據。
  3. 網絡:優化網絡帶寬和延遲,確保分佈式集群間的數據傳輸效率。

配置優化

  1. 設置合適的分區策略:根據時間或其他關鍵維度分區,提高查詢性能。
  2. 合併設置:配置合適的 merge_tree 設置,優化數據合併過程,減少碎片。
  3. 緩存和內存設置:調整 mark_cache_sizemax_memory_usage 等參數,提升緩存命中率和內存使用效率。

查詢優化

  1. 索引:利用主鍵和二級索引,加速查詢。
  2. 並行查詢:啟用 max_threads 參數,充分利用多核 CPU 並行處理查詢。
  3. 物化視圖:預計算常用查詢結果,減少實時計算開銷。

數據模型優化

  1. 列存儲設計:儘量將頻繁查詢的列存儲在一起,減少 I/O 開銷。
  2. 壓縮算法:選擇合適的壓縮算法,如 LZ4ZSTD,在壓縮率和性能之間取得平衡。

實踐案例

以量化金融中的市場數據分析為例,優化 ClickHouse 的關鍵步驟如下:

  1. 分區策略:按日期分區,使查詢特定時間段數據時更高效。
  2. 物化視圖:預計算每日交易量、價格波動等關鍵指標,減少實時計算負擔。
  3. 並行查詢:調整 max_threads,確保查詢時充分利用服務器多核資源。

監控和維護

  1. 監控工具:使用 ClickHouse 提供的 system 表監控系統性能和查詢效率。
  2. 定期維護:定期檢查並優化分區和索引,防止性能下降。

通過合理的硬件配置、優化查詢和數據模型設計,以及持續的監控和維護,ClickHouse 可以在量化金融領域中提供卓越的性能和可靠性,支持高頻交易、實時數據分析等應用場景。以下我將介紹一些優化實例:

24.4.1. 硬件配置
<clickhouse>
    <storage_configuration>
        <disks>
            <default>
                <path>/var/lib/clickhouse/</path>
            </default>
            <ssd>
                <type>local</type>
                <path>/mnt/ssd/</path>
            </ssd>
        </disks>
    </storage_configuration>
</clickhouse>
24.4.2. 創建分區表
CREATE TABLE market_data (
    date Date,
    symbol String,
    price Float64,
    volume UInt64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(date)
ORDER BY (symbol, date);
24.4.3. 使用物化視圖
CREATE MATERIALIZED VIEW market_summary
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(date)
ORDER BY (symbol, date)
AS
SELECT
    symbol,
    toYYYYMM(date) as month,
    avg(price) as avg_price,
    sum(volume) as total_volume
FROM market_data
GROUP BY symbol, month;
24.4.4. 並行查詢
SET max_threads = 8;

SELECT
    symbol,
    avg(price) as avg_price
FROM market_data
WHERE date >= '2023-01-01' AND date <= '2023-12-31'
GROUP BY symbol;
24.4.5. 壓縮算法
CREATE TABLE compressed_data (
    date Date,
    symbol String,
    price Float64,
    volume UInt64
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(date)
ORDER BY (symbol, date)
SETTINGS index_granularity = 8192,
    compress_on_write = 1,
    compression = 'lz4';

通過合理的硬件配置、優化查詢和數據模型設計,以及持續的監控和維護,ClickHouse 可以在量化金融領域中提供卓越的性能和可靠性,支持高頻交易、實時數據分析等應用場景。

案例1 通過Rust腳本在Clickhouse數據庫中建表、刪表、查詢

在量化金融領域中,使用 Rust 腳本管理 ClickHouse 數據庫可以實現高效的數據處理和管理。以下是一個基本案例 。

準備工作

首先,確保在你的 Cargo.toml 中添加 clickhouse 依賴:

[dependencies]
clickhouse = { default-features = false, version = "0.11.6" }

創建 ClickHouse 客戶端

定義並初始化一個 ClickHouse 客戶端:

#![allow(unused)]
fn main() {
use clickhouse::{Client, Row};
use lazy_static::lazy_static;

lazy_static! {
    pub static ref CLICKHOUSE_CLIENT: ClickHouseClient = ClickHouseClient::new();
}

pub struct ClickHouseClient {
    pub client: Client,
}

impl ClickHouseClient {
    pub fn new() -> Self {
        let client = Client::default().with_url("http://localhost:8123").with_database("default");
        ClickHouseClient { client }
    }
}
}

創建表

創建一個新的表 market_data

#![allow(unused)]
fn main() {
impl ClickHouseClient {
    pub async fn create_table(&self) -> Result<(), clickhouse::error::Error> {
        let query = r#"
            CREATE TABLE market_data (
                date Date,
                symbol String,
                price Float64,
                volume UInt64
            ) ENGINE = MergeTree()
            PARTITION BY date
            ORDER BY (symbol, date)
        "#;

        self.client.query(query).execute().await
    }
}
}

刪除表

刪除一個已存在的表:

#![allow(unused)]
fn main() {
impl ClickHouseClient {
    pub async fn drop_table(&self, table_name: &str) -> Result<(), clickhouse::error::Error> {
        let query = format!("DROP TABLE IF EXISTS {}", table_name);
        self.client.query(&query).execute().await
    }
}
}

查詢數據

從表中查詢數據:

#![allow(unused)]
fn main() {
#[derive(Debug, Serialize, Deserialize, Row)]
pub struct MarketData {
    pub date: String,
    pub symbol: String,
    pub price: f64,
    pub volume: u64,
}

impl ClickHouseClient {
    pub async fn query_data(&self) -> Result<Vec<MarketData>, clickhouse::error::Error> {
        let query = "SELECT * FROM market_data LIMIT 10";
        let result = self.client.query(query).fetch_all::<MarketData>().await?;
        Ok(result)
    }
}
}

示例用法

完整的示例代碼展示瞭如何使用這些功能:

#[tokio::main]
async fn main() -> Result<(), clickhouse::error::Error> {
    let client = ClickHouseClient::new();

    // 創建表
    client.create_table().await?;
    println!("Table created successfully.");

    // 查詢數據
    let data = client.query_data().await?;
    for row in data {
        println!("{:?}", row);
    }

    // 刪除表
    client.drop_table("market_data").await?;
    println!("Table dropped successfully.");

    Ok(())
}

通過這個基本教程,你可以在 Rust 腳本中實現對 ClickHouse 數據庫的基本管理操作。這些示例代碼可以根據具體需求進行擴展和優化,以滿足量化金融領域的複雜數據處理需求。

案例2 創建布林帶表的 SQL 腳本示例

本案例展示如何利用 Rust 腳本與 ClickHouse 交互,計算布林帶 (Bollinger Bands) 和其他技術指標,幫助金融分析師和量化交易員優化他們的交易策略。

-- 創建名為 AG2305_TEST 的表,使用 MergeTree 引擎
CREATE TABLE AG2305_TEST
    ENGINE = MergeTree()
        ORDER BY (minute, mean_lastprice) -- 按 minute 和 mean_lastprice 排序
AS
-- 從子查詢中選擇字段
SELECT outer_query.minute,
       CAST(outer_query.mean_lastprice AS Float32) AS mean_lastprice, -- 將 mean_lastprice 轉換為 Float32 類型
       CAST(sma AS Float32)                        AS sma,           -- 將 sma 轉換為 Float32 類型
       CAST(stddev AS Float32)                     AS stddev,        -- 將 stddev 轉換為 Float32 類型
       CAST(upper AS Float32)                      AS upper,         -- 將 upper 轉換為 Float32 類型
       CAST(lower AS Float32)                      AS lower,         -- 將 lower 轉換為 Float32 類型
       CAST(super_upper AS Float32)                AS super_upper,   -- 將 super_upper 轉換為 Float32 類型
       CAST(super_lower AS Float32)                AS super_lower,   -- 將 super_lower 轉換為 Float32 類型
       CAST(ssuper_upper AS Float32)               AS ssuper_upper,  -- 將 ssuper_upper 轉換為 Float32 類型
       CAST(ssuper_lower AS Float32)               AS ssuper_lower,  -- 將 ssuper_lower 轉換為 Float32 類型
       CASE
           -- 根據價格突破的不同情況賦值 bollinger_band_status
           WHEN super_upper > mean_lastprice AND mean_lastprice > upper THEN 1 -- 向上突破2個標準差
           WHEN super_lower < mean_lastprice AND mean_lastprice < lower THEN 2 -- 向下突破2個標準差
           WHEN ssuper_upper > mean_lastprice AND mean_lastprice > super_upper THEN 3 -- 向上突破4個標準差
           WHEN ssuper_lower < mean_lastprice AND mean_lastprice < super_lower THEN 4 -- 向下突破4個標準差
           WHEN mean_lastprice > ssuper_upper THEN 5 -- 向上突破5個標準差
           WHEN mean_lastprice < ssuper_lower AND upper != lower THEN 6 -- 向下突破5個標準差
           ELSE 0 -- 在布林帶內及其他情況
       END AS bollinger_band_status
FROM (
    -- 從內層查詢中選擇字段
    SELECT subquery.minute,
           subquery.mean_lastprice,
           subquery.start_time,
           -- 計算簡單移動平均線 (SMA)
           avg(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS sma,
           -- 計算標準差
           stddevPop(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS stddev,
           -- 計算布林帶上下軌
           sma + 1.5 * stddevPop(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS upper,
           sma - 1.5 * stddevPop(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS lower,
           -- 計算更高和更低的布林帶軌道
           sma + 3 * stddevPop(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS super_upper,
           sma - 3 * stddevPop(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS super_lower,
           sma + 4 * stddevPop(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS ssuper_upper,
           sma - 4 * stddevPop(subquery.mean_lastprice)
               OVER (PARTITION BY subquery.start_time ORDER BY subquery.minute ASC) AS ssuper_lower
    FROM (
        -- 最內層查詢,按分鐘聚合數據並計算均價
        SELECT toStartOfMinute(datetime) AS minute,
               AVG(lastprice)            AS mean_lastprice,
               -- 計算開始時間,根據 datetime 決定是當前日期的 21 點還是前一天的 21 點
               CASE
                   WHEN toHour(datetime) < 21 THEN toDate(datetime) - INTERVAL 1 DAY + INTERVAL 21 HOUR
                   ELSE toDate(datetime) + INTERVAL 21 HOUR
               END AS start_time
        FROM futures.AG2305
        GROUP BY minute, start_time
    ) subquery
) outer_query
ORDER BY outer_query.minute;

腳本解析

  1. 創建表:使用 MergeTree 引擎創建表 AG2305_TEST,並按 minutemean_lastprice 排序。
  2. 選擇字段:從內部查詢中選擇字段並進行類型轉換。
  3. 布林帶狀態:根據價格與布林帶上下軌的關係,確定 bollinger_band_status 的值。
  4. 內部查詢
    • 子查詢:聚合每分鐘的平均價格 mean_lastprice,並計算開始時間 start_time
    • 布林帶計算:計算簡單移動平均線(SMA)和不同標準差倍數的上下軌。

通過這個逐行註釋的 SQL 腳本,可以更好地理解如何在 ClickHouse 中創建複雜的計算表,並應用於量化金融領域的數據處理。

Chapter 25 - Unsafe

unsafe 關鍵字是 Rust 中的一個特性,允許你編寫不受 Rust 安全性檢查保護的代碼塊。使用 unsafe 可以執行一些不安全的操作,如手動管理內存、繞過借用檢查、執行原生指針操作等。它為你提供了更多的靈活性,但也增加了出現內存不安全和其他錯誤的風險。

以下是 unsafe 在 Rust 中的一些典型應用:

  1. 手動管理內存:使用 unsafe 可以手動分配和釋放內存,例如使用 mallocfree 類似的操作。這在編寫操作系統、嵌入式系統或需要精細控制內存的高性能應用中很有用。

  2. 原生指針unsafe 允許你使用原生指針(raw pointers),如裸指針(*const T*mut T)來進行底層內存操作。這包括解引用、指針算術和類型轉換等。

  3. 繞過借用檢查:有時候,你可能需要在某些情況下繞過 Rust 的借用檢查規則,以實現一些特殊的操作,如跨函數傳遞可變引用。

  4. 調用外部代碼:當與其他編程語言(如 C 或 C++)進行交互時,你可能需要使用 unsafe 來調用外部的不受 Rust 控制的代碼。這包括編寫 Rust 綁定以與 C 庫進行交互。

  5. 多線程編程unsafe 有時候用於多線程編程,以管理共享狀態、原子操作和同步原語。這包括 std::syncstd::thread 中的一些功能。

需要注意的是,使用 unsafe 需要非常小心,因為它可以導致內存不安全、數據競爭和其他嚴重的錯誤。Rust 的安全性特性是它的一大賣點,unsafe 的使用應該被限制在必要的情況下,並且必須經過仔細的審查和測試。在實際編程中,大多數情況下都可以避免使用 unsafe,因為 Rust 提供了強大的工具來確保代碼的安全性和正確性。只有在需要訪問底層系統資源、進行高性能優化或與外部代碼交互等特殊情況下,才應該考慮使用 unsafe

在金融領域,Rust 的 unsafe 關鍵字通常需要謹慎使用,因為金融系統涉及到重要的安全性和可靠性要求。unsafe 允許繞過 Rust 的安全檢查和規則,這意味著你需要更加小心地管理代碼,以確保它不會導致內存不安全或其他安全性問題。

以下是在金融領域中可能使用 unsafe 的一些場景和用例:

  1. 與外部系統集成:金融系統通常需要與底層硬件、操作系統、網絡庫等進行交互。在這些情況下,unsafe 可能用於編寫與外部 C 代碼進行交互的 Rust 綁定,以確保正確的內存佈局和數據傳遞。

  2. 性能優化:金融計算通常涉及大量數據處理,對性能要求較高。在某些情況下,使用 unsafe 可能允許你進行底層內存操作或使用不安全的優化技巧,以提高計算性能。

  3. 數據結構的自定義實現:金融領域可能需要定製的數據結構,以滿足特定的需求。在這種情況下,unsafe 可能用於實現自定義數據結構,但必須確保這些結構是正確和安全的。

  4. 低級別的多線程編程:金融系統通常需要高度併發的處理能力。在處理多線程和併發性時,可能需要使用 unsafe 來管理線程間的共享狀態和同步原語,但必須小心避免數據競爭和其他多線程問題。

無論在金融領域還是其他領域,使用 unsafe 都需要嚴格的代碼審查和測試,以確保代碼的正確性和安全性。在金融領域特別需要保持高度的可信度,因此必須格外小心,遵循最佳實踐,使用 unsafe 的時機應該非常明確,並且必須有充分的理由。另外,金融領域通常受到監管和合規性要求,這也需要確保代碼的安全性和穩定性。因此,unsafe 應該謹慎使用,只在真正需要時才使用,並且應該由經驗豐富的工程師來管理和審查。

在量化金融領域,有些情況下確實需要使用 unsafe 來執行一些底層操作,尤其是在與外部 C/C++ 庫進行交互時。一個常見的案例是與某些量化金融庫或市場數據提供商的 C/C++ API 進行集成。以下是一個示例,展示瞭如何在 Rust 中與外部 C/C++ 金融庫進行交互,可能需要使用 unsafe

案例:與外部金融庫的交互

假設你的量化金融策略需要獲取市場數據,但市場數據提供商只提供了 C/C++ API。在這種情況下,你可以編寫一個 Rust 綁定,以便在 Rust 中調用外部 C/C++ 函數。

首先,你需要創建一個 Rust 項目,並設置一個用於與外部庫交互的 Rust 模塊。然後,創建一個 Rust 綁定,將外部庫的函數聲明和數據結構導入到 Rust 中。這可能涉及到使用 extern 關鍵字和 unsafe 代碼塊來調用外部函數。

以下是一個簡化的示例:

// extern聲明,將外部庫中的函數導入到Rust中
extern "C" {
    fn get_stock_price(symbol: *const u8) -> f64;
    // 還可以導入其他函數和數據結構
}

// 調用外部函數的Rust封裝
pub fn get_stock_price_rust(symbol: &str) -> Option<f64> {
    let c_symbol = CString::new(symbol).expect("CString conversion failed");
    let price = unsafe { get_stock_price(c_symbol.as_ptr()) };
    if price < 0.0 {
        None
    } else {
        Some(price)
    }
}

fn main() {
    let symbol = "AAPL";
    if let Some(price) = get_stock_price_rust(symbol) {
        println!("The stock price of {} is ${:.2}", symbol, price);
    } else {
        println!("Failed to retrieve the stock price for {}", symbol);
    }
}

在這個示例中,我們假設有一個外部 C/C++ 函數 get_stock_price,它獲取股票代碼並返回股價。我們使用 extern "C" 聲明將其導入到 Rust 中,並在 get_stock_price_rust 函數中使用 unsafe 調用它。

這個示例展示了在量化金融中可能需要使用 unsafe 的情況,因為你必須管理外部 C/C++ 函數的調用以及與它們的交互。在這種情況下,你需要確保 unsafe 代碼塊中的操作是正確且安全的,並且進行了適當的錯誤處理。在與外部庫進行交互時,一定要小心確保代碼的正確性和穩定性。

案例:高性能數值計算

另一個可能需要使用 unsafe 的量化金融案例是執行高性能計算和優化,特別是在需要進行大規模數據處理和數值計算時。以下是一個示例,展示瞭如何使用 unsafe 來執行高性能數值計算的情況。

假設你正在開發一個量化金融策略,需要進行大規模的數值計算,例如蒙特卡洛模擬或優化算法。在這種情況下,你可能需要使用 Rust 中的 ndarray 或其他數值計算庫來執行操作,但某些操作可能需要使用 unsafe 來提高性能。

以下是一個示例,展示瞭如何使用 unsafe 來執行矩陣操作:

use ndarray::{Array2, Axis, s};

fn main() {
    // 創建一個大矩陣
    let size = 1000;
    let mut matrix = Array2::zeros((size, size));

    // 使用 unsafe 來執行高性能操作
    unsafe {
        // 假設這是一個計算密集型的操作
        for i in 0..size {
            for j in 0..size {
                *matrix.uget_mut((i, j)) = i as f64 * j as f64;
            }
        }
    }

    // 執行其他操作
    let row_sum = matrix.sum_axis(Axis(0));
    let max_value = matrix.fold(0.0, |max, &x| if x > max { x } else { max });

    println!("Row sum: {:?}", row_sum);
    println!("Max value: {:?}", max_value);
}

在這個示例中,我們使用 ndarray 庫創建了一個大矩陣,並使用 unsafe 塊來執行計算密集型的操作以填充矩陣。這個操作假設你已經確保了正確性和安全性,因此可以使用 unsafe 來提高性能。

需要注意的是,使用 unsafe 應該非常小心,必須確保操作是正確的且不會導致內存不安全。在實際應用中,你可能需要使用更多的數值計算庫和優化工具,但 unsafe 可以在某些情況下提供額外的性能優勢。無論如何,對於量化金融策略,正確性和可維護性始終比性能更重要,因此使用 unsafe 應該謹慎,並且必須小心驗證和測試代碼。

Chapter 26 - 文檔和測試

26.1 文檔註釋

在 Rust 中,文檔的編寫主要使用文檔註釋(Doc Comments)和 Rustdoc 工具來生成文檔。文檔註釋以 /////! 開始,通常位於函數、模塊、結構體、枚舉等聲明的前面。以下是 Rust 中文檔編寫的基本寫法和示例:

  1. 文檔註釋格式

    文檔註釋通常遵循一定的格式,包括描述、用法示例、參數說明、返回值說明等。下面是一個通用的文檔註釋格式示例:

    #![allow(unused)]
    fn main() {
    /// This is a description of what the item does.
    ///
    /// # Examples
    ///
    /// ```
    /// let result = my_function(arg1, arg2);
    /// assert_eq!(result, expected_value);
    /// ```
    ///
    /// ## Parameters
    ///
    /// - `arg1`: Description of the first argument.
    /// - `arg2`: Description of the second argument.
    ///
    /// ## Returns
    ///
    /// Description of the return value.
    ///
    /// # Panics
    ///
    /// Description of panic conditions, if any.
    ///
    /// # Errors
    ///
    /// Description of possible error conditions, if any.
    ///
    /// # Safety
    ///
    /// Explanation of any unsafe code or invariants.
    pub fn my_function(arg1: Type1, arg2: Type2) -> ReturnType {
        // Function implementation
    }
    }

    在上面的示例中,文檔註釋包括描述、用法示例、參數說明、返回值說明以及可能的 panic 和錯誤情況的描述。

  2. 生成文檔

    為了生成文檔,你可以使用 Rust 內置的文檔生成工具 Rustdoc。運行以下命令來生成文檔:

    cargo doc
    

    這將生成文檔並將其保存在項目目錄的 target/doc 文件夾下。你可以在瀏覽器中打開生成的文檔(位於 target/doc 中的 index.html 文件)來查看你的代碼文檔。

  3. 鏈接到其他項

    你可以在文檔中鏈接到其他項,如函數、模塊、結構體等,以便創建交叉引用。使用 [] 符號來創建鏈接,例如 [my_function] 將鏈接到名為 my_function 的項。

  4. 測試文檔示例

    你可以通過運行文檔測試來確保文檔中的示例代碼是有效的。運行文檔測試的命令是:

    cargo test --doc
    

    這將運行文檔中的所有示例代碼,確保它們仍然有效。

  5. 文檔主題

    你可以使用 Markdown 語法來美化文檔。Rustdoc支持Markdown,所以你可以使用標題、列表、代碼塊、鏈接等Markdown元素來組織文檔並增強其可讀性。

文檔編寫是開發過程中的重要部分,它幫助你的代碼更易於理解、使用和維護。好的文檔不僅對其他開發人員有幫助,還有助於你自己更容易回顧和理解代碼。因此,確保在 Rust 項目中編寫清晰和有用的文檔是一個良好的實踐。

26.1 單元測試

Rust 是一種系統級編程語言,它鼓勵編寫高性能和安全的代碼。為了確保代碼的正確性,Rust 提供了一套強大的測試工具,包括單元測試、集成測試和屬性測試。在這裡,我們將詳細介紹 Rust 的單元測試。

單元測試是一種測試方法,用於驗證代碼的各個單元(通常是函數或方法)是否按預期工作。在 Rust 中,單元測試通常包括編寫測試函數,然後使用 #[cfg(test)] 屬性標記它們,以便只在測試模式下編譯和運行。

以下是 Rust 單元測試的詳細解釋:

  1. 創建測試函數

    在 Rust 中,測試函數的命名通常以 test 開頭,後面跟著描述性的函數名。測試函數應該返回 ()(unit 類型),因為它們通常不返回任何值。測試函數可以使用 assert! 宏或其他斷言宏來檢查代碼的行為是否與預期一致。例如:

    #![allow(unused)]
    fn main() {
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_addition() {
            assert_eq!(2 + 2, 4);
        }
    }
    }

    在這個示例中,我們有一個名為 test_addition 的測試函數,它使用 assert_eq! 宏來斷言 2 + 2 的結果是否等於 4。如果不等於 4,測試將失敗。

  2. 使用 #[cfg(test)] 標誌

    在 Rust 中,你可以使用 #[cfg(test)] 屬性將測試代碼標記為僅在測試模式下編譯和運行。這可以防止測試代碼影響生產代碼的性能和大小。在示例中,我們在測試模塊中使用了 #[cfg(test)]

  3. 運行測試

    要運行測試,可以使用 Rust 的測試運行器,通常是 cargo test 命令。在你的項目根目錄下,運行 cargo test 將運行所有標記為測試的函數。測試運行器將輸出測試結果,包括通過的測試和失敗的測試。

  4. 添加更多測試

    你可以在測試模塊中添加任意數量的測試函數,以驗證你的代碼的不同部分。測試函數應該覆蓋你的代碼中的各種情況和邊界條件,以確保代碼的正確性。

  5. 測試斷言宏

    Rust 提供了許多測試斷言宏,如 assert_eq!assert_ne!assert!assert_approx_eq! 等,以適應不同的測試需求。你可以根據需要選擇適當的宏來編寫測試。

  6. 測試組織

    你可以在不同的模塊中組織你的測試,以使測試代碼更清晰和易於管理。測試模塊可以嵌套,以反映你的代碼組織結構。

單元測試在量化金融領域具有重要的意義,它有助於確保量化金融代碼的正確性、穩定性和可維護性:

  1. 驗證金融模型和算法的正確性:在量化金融領域,代碼通常涉及複雜的金融模型和算法。通過編寫單元測試,可以驗證這些模型和算法是否按照預期工作,從而提高了金融策略的可靠性。
  2. 捕獲潛在的問題:單元測試可以幫助捕獲潛在的問題和錯誤,包括數值計算錯誤、邊界情況處理不當、算法邏輯錯誤等。這有助於在生產環境中避免意外的風險和損失。
  3. 快速反饋:單元測試提供了快速反饋的機制。當開發人員進行代碼更改時,單元測試可以自動運行,並迅速告訴開發人員是否破壞了現有的功能。這有助於迅速修復問題,減少了錯誤的傳播。
  4. 確保代碼的可維護性:單元測試通常要求編寫模塊化和可測試的代碼。這鼓勵開發人員編寫清晰、簡潔和易於理解的代碼,從而提高了代碼的可維護性。
  5. 支持重構和優化:通過具有完善的單元測試套件,開發人員可以更加放心地進行代碼重構和性能優化。單元測試可以確保在這些過程中不會破壞現有的功能。

所以單元測試在量化金融領域是一種關鍵的質量保證工具。通過合理編寫和維護單元測試,可以降低金融策略的風險,提高交易系統的可靠性,並促進團隊的協作和知識共享。因此,在量化金融領域,單元測試被認為是不可或缺的開發實踐。

26.2 文檔測試

文檔測試是 Rust 中一種特殊類型的測試,它與單元測試有所不同。文檔測試主要用於驗證文檔中的代碼示例是否有效,可以作為文檔的一部分運行。這些測試以 cargo test 命令運行,但它們會在文檔構建期間執行,以確保示例代碼仍然有效。以下是如何編寫和運行文檔測試的詳細步驟:

  1. 編寫文檔註釋

    在你的 Rust 代碼中,你可以使用特殊的註釋塊 /////! 來編寫文檔註釋。在文檔註釋中,你可以包括代碼示例,如下所示:

    #![allow(unused)]
    fn main() {
    /// This function adds two numbers.
    ///
    /// # Examples
    ///
    /// ```
    /// let result = add(2, 3);
    /// assert_eq!(result, 5);
    /// ```
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    }

    在上面的示例中,我們編寫了一個名為 add 的函數,並使用文檔註釋包含了一個示例。

  2. 運行文檔測試

    要運行文檔測試,你可以使用 cargo test 命令,幷包括 --doc 標誌:

    cargo test --doc
    

    運行後,Cargo 將執行文檔測試並輸出結果。它將查找文檔註釋中的示例,並嘗試運行這些示例。如果示例中的代碼成功運行且產生的輸出與註釋中的示例匹配,測試將通過。

  3. 檢查文檔測試結果

    文檔測試的結果將包括通過的測試示例和失敗的測試示例。你應該檢查輸出以確保示例代碼仍然有效。如果有失敗的示例,你需要檢查並修復文檔或代碼中的問題。

文檔測試(Document Testing)在量化金融領域具有重要的意義,它不僅有助於確保代碼的正確性,還有助於提高代碼的可維護性和可理解性。以下是文檔測試在量化金融中的一些重要意義:

  1. 驗證金融模型的正確性:量化金融領域涉及複雜的金融模型和算法。文檔測試可以用於驗證這些模型的正確性,確保它們按照預期工作。通過在文檔中提供示例和預期結果,可以確保模型在代碼實現中與理論模型一致。

  2. 示例和文檔:文檔測試的結果可以成為代碼文檔的一部分,提供示例和用法說明。這對於其他開發人員、研究人員和用戶來說是非常有價值的,因為他們可以輕鬆地查看代碼示例,瞭解如何使用量化金融工具和庫。

  3. 改進代碼可讀性:編寫文檔測試通常需要清晰的文檔註釋和示例代碼,這有助於提高代碼的可讀性和可理解性。通過清晰的註釋和示例,其他人可以更容易地理解代碼的工作原理,降低了學習和使用的難度。

  4. 快速反饋:文檔測試是一種快速獲得反饋的方式。當你修改代碼時,文檔測試可以自動運行,並告訴你是否破壞了現有的功能或預期結果。這有助於快速捕獲潛在的問題並進行修復。

  5. 合規性和審計:在金融領域,合規性和審計是非常重要的。文檔測試可以作為合規性和審計過程的一部分,提供可追溯的證據,證明代碼的正確性和穩定性。

  6. 教育和培訓:文檔測試還可以用於培訓和教育目的。新入職的開發人員可以通過查看文檔測試中的示例和註釋來快速瞭解代碼的工作方式和最佳實踐。

總之,文檔測試在量化金融領域具有重要意義,它不僅有助於驗證代碼的正確性,還提供了示例、文檔、可讀性和合規性的好處。通過合理使用文檔測試,可以提高量化金融代碼的質量,減少錯誤和問題,並增強代碼的可維護性和可理解性。

26.3 項目集成測試

Rust 項目的集成測試通常用於測試不同模塊之間的交互,以確保整個項目的各個部分正常協作。與單元測試不同,集成測試涵蓋了更廣泛的範圍,通常測試整個程序的功能而不是單個函數或模塊。以下是在 Rust 項目中進行集成測試的一般步驟:

  1. 創建測試文件

    集成測試通常與項目的源代碼分開,因此你需要創建一個專門的測試文件夾和測試文件。一般來說,測試文件的命名約定是 tests 文件夾下的文件以 .rs 擴展名結尾,並且測試模塊應該使用 mod 關鍵字定義。

    創建一個測試文件,例如 tests/integration_test.rs

  2. 編寫集成測試

    在測試文件中,你可以編寫測試函數來測試整個項目的功能。這些測試函數應該模擬實際的應用場景,包括模塊之間的交互。你可以使用 Rust 的標準庫中的 assert! 宏或其他斷言宏來驗證代碼的行為是否與預期一致。

    #![allow(unused)]
    fn main() {
    // tests/integration_test.rs
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn test_whole_system() {
            // 模擬整個系統的交互
            let result = your_project::function1() + your_project::function2();
            assert_eq!(result, 42);
        }
    }
    }

    在這個示例中,我們有一個名為 test_whole_system 的集成測試函數,它測試整個系統的行為。

  3. 配置測試環境

    在集成測試中,你可能需要配置一些測試環境,以模擬實際應用中的情況。這可以包括初始化數據庫、設置配置選項等。

  4. 運行集成測試

    使用 cargo test 命令來運行項目的集成測試:

    cargo test --test integration_test
    

    這將運行名為 integration_test 的測試文件中的所有集成測試函數。

  5. 檢查測試結果

    檢查測試運行的結果,包括通過的測試和失敗的測試。如果有失敗的測試,你需要檢查並修復與項目的整合相關的問題。

項目集成測試在 Rust 量化金融中具有關鍵的意義,它有助於確保整個量化金融系統在各個組件之間協同工作,並滿足業務需求。以下是項目集成測試不可或缺的的原因:

  1. 驗證整個系統的一致性:量化金融系統通常由多個組件組成,包括數據採集、模型計算、交易執行等。項目集成測試可以確保這些組件在整個系統中協同工作,並保持一致性。它有助於檢測潛在的集成問題,例如數據流傳輸、算法接口等。

  2. 模擬真實市場環境:項目集成測試可以模擬真實市場環境,包括不同市場條件、波動性和交易活動水平。這有助於評估系統在各種市場情況下的性能和可靠性。

  3. 檢測潛在風險:量化金融系統必須具備高度的可靠性,以避免潛在的風險和損失。項目集成測試可以幫助檢測潛在的風險,例如系統崩潰、錯誤的交易執行等。

  4. 評估系統性能:集成測試可以用於評估系統的性能,包括響應時間、吞吐量和穩定性。這有助於確定系統是否能夠在高負載下正常運行。

  5. 測試策略的執行:量化金融策略可能包括多個組件,包括數據處理、信號生成、倉位管理和風險控制等。項目集成測試可以確保整個策略的執行符合預期。

  6. 合規性和審計:在金融領域,合規性和審計非常重要。項目集成測試可以提供可追溯性和審計的證據,以確保系統在合規性方面達到要求。

  7. 自動化測試流程:通過自動化項目集成測試流程,可以快速發現問題並降低測試成本。自動化測試還可以在每次代碼變更後持續運行,以捕獲潛在問題。

  8. 改進系統可維護性:項目集成測試通常需要將系統的不同部分解耦合作,這有助於改進系統的可維護性。通過強調接口和模塊化設計,可以使系統更容易維護和擴展。

項目集成測試在 Rust 量化金融中的意義在於確保系統的正確性、穩定性和性能,同時降低風險並提高系統的可維護性。這是構建高度可信賴的金融系統所必需的實踐,有助於確保交易策略在實際市場中能夠可靠執行。

最後,讓我們來對比以下三種測試的異同,以下是 Rust 中單元測試、文檔測試和集成測試的對比表格:

特徵單元測試文檔測試集成測試
目的驗證代碼的單個單元(通常是函數或方法)是否按預期工作驗證文檔中的代碼示例是否有效驗證整個項目的各個部分是否正常協作
代碼位置通常與生產代碼位於同一文件中(測試模塊)嵌入在文檔註釋中通常位於項目的測試文件夾中,與生產代碼分開
運行方式使用 cargo test 命令運行使用 cargo test --doc 命令運行使用 cargo test 命令運行,指定測試文件
測試範圍通常測試單個函數或模塊的功能驗證文檔中的代碼示例測試整個項目的不同部分之間的交互
斷言宏使用斷言宏如 assert_eq!assert_ne!assert!使用斷言宏如 assert_eq!assert_ne!assert!使用斷言宏如 assert_eq!assert_ne!assert!
測試目標確保單元的正確性確保文檔中的示例代碼正確性確保整個項目的功能和協作正確性
測試環境通常不需要額外的測試環境可能需要模擬一些環境或配置可能需要配置一些測試環境,如數據庫、配置選項等
分離性通常與生產代碼分開,但位於同一文件中與文檔和代碼緊密集成,位於文檔註釋中通常與生產代碼分開,位於測試文件中
自動化通常在開發流程中頻繁運行,可自動化通常在文檔構建時運行,可自動化通常在開發流程中運行,可自動化
用途驗證代碼功能是否正確驗證示例代碼是否有效驗證整個項目的各個部分是否正常協作

請注意,這些測試類型通常用於不同的目的和測試場景。單元測試主要用於驗證單個函數或模塊的功能,文檔測試用於驗證文檔中的示例代碼,而集成測試用於驗證整個項目的功能和協作。在實際開發中,你可能會同時使用這三種測試類型來確保代碼的質量和可維護性。

Chapter 27 常見技術指標及其實現

量化金融技術指標通常用於分析和預測金融市場的走勢和價格變動。以下是一些常見的量化金融技術指標:

以下是關於各種常見技術指標的信息,包括它們的名稱、描述以及主要用途:

技術指標描述主要用途
移動平均線(Moving Averages)包括簡單移動平均線(SMA)和指數移動平均線(EMA),用於平滑價格數據以識別趨勢。識別價格趨勢和確定趨勢的方向。
相對強度指標(RSI)衡量市場超買和超賣情況,用於判斷價格是否過度波動。識別市場的超買和超賣情況,判斷價格是否具備反轉潛力。
隨機指標(Stochastic Oscillator)用於測量價格相對於其價格範圍的位置,以確定超買和超賣情況。識別資產的超買和超賣情況,產生買賣信號。
布林帶(Bollinger Bands)通過在價格周圍繪製波動性通道來識別價格波動性和趨勢。識別價格波動性,確定支撐和阻力水平。
MACD指標(Moving Average Convergence Divergence)結合不同期限的移動平均線以識別價格趨勢的強度和方向。識別價格的趨勢、方向和潛在的交叉點。
隨機強度指標(RSI)衡量一種資產相對於市場指數的表現。評估資產的相對強度和相對弱點。
ATR指標(Average True Range)測量資產的波動性,幫助確定止損和止盈水平。評估資產的波動性,確定適當的風險管理策略。
ADX指標(Average Directional Index)衡量趨勢的強度和方向。識別市場趨勢的強度和方向,幫助決策進出場時機。
ROC指標(Rate of Change)衡量價格百分比變化以識別趨勢的加速或減速。識別價格趨勢的速度變化,潛在的反轉或加速。
CCI指標(Commodity Channel Index)用於識別價格相對於其統計平均值的偏離。評估資產是否處於超買或超賣狀態。
Fibonacci回調和擴展水平基於黃金比例的數學工具,用於預測支撐和阻力水平。識別潛在的支撐和阻力水平,幫助決策進出場時機。
成交量分析指標包括成交量柱狀圖和成交量移動平均線,用於分析市場的活躍度和力量。評估市場活躍度,輔助價格趨勢分析。
均線交叉通過不同週期的移動平均線的交叉來識別買入和賣出信號。識別趨勢的改變,產生買賣信號。
Ichimoku雲提供了有關趨勢、支撐和阻力水平的綜合信息。提供多個指標的綜合信息,幫助識別趨勢和支撐/阻力水平。
威廉指標(Williams %R)類似於隨機指標,用於測量超買和超賣情況。評估資產是否處於超買或超賣狀態,產生買賣信號。
均幅指標(Average Directional Movement Index,ADX)用於確定趨勢的方向和強度。識別市場的趨勢方向和趨勢的強度,幫助決策進出場時機。
多重時間框架分析(Multiple Time Frame Analysis)同時使用不同時間週期的圖表來確認趨勢。提供更全面的市場分析,減少錯誤信號的可能性。

這些技術指標是量化金融和股票市場分析中常用的工具,交易者使用它們來幫助做出買入和賣出決策,評估市場趨勢和風險,並制定有效的交易策略。根據市場情況和交易者的需求,可以選擇使用其中一個或多個指標來進行分析。

通常各個主要編程語言都有用於技術分析(Technical Analysis,TA)的庫和工具,用於在金融市場數據上執行各種技術指標和分析。在C、Go和Python中常見的TA庫一般有這些:

C語言:

  1. TA-Lib(Technical Analysis Library): TA-Lib是一種廣泛使用的C庫,提供了超過150種技術指標和圖表模式的計算功能。它支持各種不同類型的金融市場數據,並且可以輕鬆與C/C++項目集成。

Go語言:

  1. tulipindicators: tulipindicators是一個用Go編寫的開源技術指標庫,它提供了多種常用技術指標的實現。這個庫易於使用,可以在Go項目中方便地集成。
  2. **go-talib:**ta的go語言wrapper

Python語言:

  1. Pandas TA: Pandas TA是一個基於Python的庫,構建在Pandas DataFrame之上,它提供了超過150個技術指標的計算功能。Pandas TA與Pandas無縫集成,使得在Python中進行金融數據分析變得非常方便。
  2. TA-Lib for Python: 與C版本類似,TA-Lib也有適用於Python的接口,允許Python開發者使用TA-Lib中的技術指標。這個庫通過綁定C庫的方式實現了高性能。

作為量化金融系統部署的前提之一,在Rust社區的生態中,當然也具有用於技術分析的庫,雖然它的生態系統可能沒有像Python或C那樣豐富,但仍然存在一些可以用於量化金融分析的工具和庫,配合自研的技術指標庫和數學庫,在生產環境下也足夠使用。

以下是一些常見的可用於技術分析和量化金融的Rust庫,:

  1. TAlib-rs: TAlib-rs是一個Rust的TA-Lib綁定,它允許Rust開發者使用TA-Lib中的技術指標功能。TA-Lib包含了150多種技術指標的實現,因此通過TAlib-rs,你可以在Rust中執行廣泛的技術分

  2. RustQuant: Rust中的量化金融工具庫。同時也是Rust中最大、最成熟的期權定價庫。

  3. investments: 一個用Rust編寫的開源庫,旨在提供一些用於金融和投資的工具和函數。這個庫可能包括用於計算投資回報率、分析金融數據以及執行基本的投資分析的功能。

Rust在金融領域的應用確實相對較新,因此可用的庫和工具有一定的可能闕如。不過,隨著Rust的不斷發展和生態系統的壯大,我預期將會有更多的金融分析和量化交易工具出現。當你已經熟悉Rust編程,並且希望在此領域進行開發的時候,也可以考慮一下為Rust社區貢獻更多的金融相關項目和庫。

好,之前在第3章我們已經實現了SMA、EMA和RSI,現在我們來嘗試進行一些其他實用技術分析指標的rust實現。

27.1: 隨機指標(Stochastic Oscillator)

在金融市場中,很多投資者會通過嘗試識別**"超買"(Overbought)"超賣"(Oversold)**狀態並通過自己對這些狀態的應對策略來套利。 超買是指市場或特定資產的價格被認為高於其正常或合理的價值水平的情況。這通常發生在價格迅速上升後,投資者情緒變得過於樂觀,導致購買壓力增加。超買時,市場可能出現過度購買的現象,價格可能會進一步下跌或趨於平穩。而超賣是指市場或特定資產的價格被認為低於其正常或合理的價值水平的情況。這通常發生在價格迅速下跌後,投資者情緒變得過於悲觀,導致賣出壓力增加。超賣時,市場可能出現過度賣出的現象,價格可能會進一步上漲或趨於平穩。

一些技術指標如相對強度指標(RSI)或隨機指標(Stochastic Oscillator)常用來識別超買情況。當這些指標的數值超過特定閾值(通常為70~80),就被視為市場處於超買狀態,可能預示著價格的下跌。而當這些指標的數值低於特定閾值(通常為20~30),就被視為市場處於超賣狀態,可能預示著價格的上漲。

之前我們在第3章對RSI已經有所瞭解。現在我們再來學習一下隨機指標,它由George C. Lane 在20世紀50年代開發,是一種相對簡單但有效的、常用於技術分析的動量指標。

隨機指標通常由以下幾個主要組成部分構成:

  1. %K線(%K Line): %K線是當前價格與一段時間內的價格範圍的比率,通常以百分比表示。它可以用以下公式計算:

    %K = [(當前收盤價 - 最低價) / (最高價 - 最低價)] * 100

    %K線的計算結果在0到100之間波動,可以幫助識別價格相對於給定週期內的價格範圍的位置。

  2. %D線(%D Line): %D線是%K線的平滑線,通常使用移動平均線進行平滑處理。這有助於減少%K線的噪音,提供更可靠的信號。%D線通常使用簡單移動平均線(SMA)或指數移動平均線(EMA)進行計算。

  3. 超買和超賣水平: 在隨機指標中,通常會繪製兩個水平線,一個表示超買水平(通常為80),另一個表示超賣水平(通常為20)。當%K線上穿80時,表明市場可能處於超買狀態,可能會發生價格下跌。當%K線下穿20時,表明市場可能處於超賣狀態,可能會發生價格上漲。

隨機指標的典型用法包括:

  • 當%K線上穿%D線時,產生買入信號,表示價格可能上漲。
  • 當%K線下穿%D線時,產生賣出信號,表示價格可能下跌。
  • 當%K線位於超買水平以上時,可能發生賣出信號。
  • 當%K線位於超賣水平以下時,可能發生買入信號。

需要注意的是,隨機指標並不是一種絕對的買賣信號工具,而是用於輔助決策的指標。它常常與其他技術指標和分析工具一起使用,以提供更全面的市場分析。交易者還應謹慎使用隨機指標,特別是在非趨勢市場中,因為在價格範圍內波動較大時,可能會產生誤導性的信號。因此,對於每個市場環境,需要根據其他指標和分析來進行綜合判斷。

以下是Stochastic Oscillator(隨機指標)和RSI(相對強度指標)之間的主要區別:

特徵Stochastic Oscillator相對強度指標 (RSI)
類型動量指標動量指標
創建者George C. LaneJ. Welles Wilder
計算方式基於當前價格與價格範圍的比率基於平均增益和平均損失
計算結果的範圍0 到 1000 到 100
主要目的識別超買和超賣情況,以及價格趨勢變化衡量資產價格的強弱
%K線和%D線包括%K線和%D線,%D線是%K線的平滑線通常只有一個RSI線
超買和超賣水平通常在80和20之間,用於產生買賣信號通常在70和30之間,用於產生買賣信號
信號產生當%K線上穿%D線時產生買入信號,下穿時產生賣出信號當RSI線上穿70時產生賣出信號,下穿30時產生買入信號
應用領域用於識別超買和超賣情況以及價格的反轉點用於衡量資產的強弱並確定買賣時機
時間週期通常使用短期和長期週期進行計算通常使用14個交易日週期進行計算
常見用途適用於不同市場和資產類別,特別是適用於振盪市場適用於評估股票、期貨和外匯等資產的強弱

需要注意的是,雖然Stochastic Oscillator和RSI都是用於動量分析的指標,但它們的計算方式、信號產生方式和主要應用方向都略有不同。交易者可以根據自己的交易策略和市場條件選擇使用其中一個或兩者結合使用,以輔助決策。

27.2:布林帶(Bollinger Bands)

布林帶(Bollinger Bands)是一種常用於技術分析的指標,旨在幫助交易者識別資產價格的波動性和趨勢方向。它由約翰·布林格(John Bollinger)於1980年代開發,是一種基於統計學原理的工具。以下是對布林帶的詳細解釋:

布林帶的構成: 布林帶由以下三個主要部分組成:

  1. 中軌(中間線): 中軌是布林帶的中心線,通常是簡單移動平均線(SMA)。中軌的計算通常基於一段固定的時間週期,例如20個交易日的收盤價的SMA。這個中軌代表了資產價格的趨勢方向。

  2. 上軌(上限線): 上軌是位於中軌上方的線,其位置通常是中軌加上兩倍標準差(Standard Deviation)的值。標準差是一種測量數據分散程度的統計指標,用於衡量價格波動性。上軌代表了資產價格的波動性,通常用來識別價格上漲的潛力。

  3. 下軌(下限線): 下軌是位於中軌下方的線,其位置通常是中軌減去兩倍標準差的值。下軌同樣代表了資產價格的波動性,通常用來識別價格下跌的潛力。

布林帶的應用: 布林帶有以下幾個主要的應用和用途:

  1. 波動性識別: 布林帶的寬窄可以用來衡量價格波動性。帶寬收窄通常表示價格波動性較低,而帶寬擴大則表示價格波動性較高。這可以幫助交易者判斷市場的活躍度和價格趨勢的穩定性。

  2. 趨勢識別: 當價格趨勢明顯時,布林帶的上軌和下軌可以幫助確定支撐和阻力水平。當價格觸及或穿越上軌時,可能表明價格上漲趨勢強勁,而當價格觸及或穿越下軌時,可能表明價格下跌趨勢較強。

  3. 超買和超賣情況: 當價格接近或穿越布林帶的上軌時,可能表明市場處於超買狀態,因為價格偏離了其正常波動範圍。相反,當價格接近或穿越布林帶的下軌時,可能表明市場處於超賣狀態。

  4. 交易信號: 交易者經常使用布林帶產生買入和賣出信號。一種常見的策略是在價格觸及上軌時賣出,在價格觸及下軌時買入。這可以幫助捕捉價格的短期波動。

需要注意的是,布林帶是一種輔助工具,通常需要與其他技術指標和市場分析方法結合使用。交易者應謹慎使用布林帶信號,並考慮市場的整體背景和趨勢。此外,布林帶的參數(如時間週期和標準差倍數)可以根據不同市場和交易策略進行調整。

27.3:MACD指標(Moving Average Convergence Divergence)

MACD(Moving Average Convergence Divergence)是一種常用於技術分析的動量指標,用於衡量資產價格趨勢的強度和方向。它由傑拉爾德·阿佩爾(Gerald Appel)於1979年首次引入,並且在技術分析中廣泛應用。以下是對MACD指標的詳細解釋:

MACD指標的構成: MACD指標由以下三個主要組成部分構成:

  1. 快速線(Fast Line): 也稱為MACD線(MACD Line),是資產價格的短期移動平均線與長期移動平均線之間的差值。通常,快速線的計算基於12個交易日的短期移動平均線減去26個交易日的長期移動平均線。

    快速線(MACD Line) = 12日EMA - 26日EMA

    其中,EMA代表指數加權移動平均線(Exponential Moving Average),它使得近期價格對快速線的影響較大。

  2. 慢速線(Slow Line): 也稱為信號線(Signal Line),是快速線的移動平均線。通常,慢速線的計算使用快速線的9日EMA。

    慢速線(Signal Line) = 9日EMA(MACD Line)

  3. MACD柱狀圖(MACD Histogram): MACD柱狀圖表示快速線和慢速線之間的差值,用於展示價格趨勢的強度和方向。MACD柱狀圖的計算方法是:

    MACD柱狀圖 = 快速線(MACD Line) - 慢速線(Signal Line)

MACD的應用: MACD指標可以用於以下幾個方面的技術分析:

  1. 趨勢識別: 當MACD線位於慢速線上方並向上移動時,通常表示價格處於上升趨勢,這可能是買入信號。相反,當MACD線位於慢速線下方並向下移動時,通常表示價格處於下降趨勢,這可能是賣出信號。

  2. 交叉信號: 當MACD線上穿慢速線時,產生買入信號,表示價格可能上漲。當MACD線下穿慢速線時,產生賣出信號,表示價格可能下跌。

  3. 背離(Divergence): 當MACD指標與價格圖形出現背離時,可能表示趨勢的弱化或反轉。例如,如果價格創下新低而MACD柱狀圖創下高點,這可能是價格反轉的信號。

  4. 柱狀圖的觀察: MACD柱狀圖的高度可以反映價格趨勢的強度。較高的柱狀圖表示價格動能較強,較低的柱狀圖表示價格動能較弱。

需要注意的是,MACD是一種多功能的指標,可以用於不同市場和不同時間週期的分析。它通常需要與其他技術指標和市場分析方法結合使用,以提供更全面的市場信息。MACD的參數可以根據具體情況進行調整,以滿足不同的交易策略和市場條件。

27.4:ADX指標(Average Directional Index)

ADX(Average Directional Index)是一種用於技術分析的指標,旨在衡量資產價格趨勢的強度和方向。ADX是由威爾斯·威爾德(Welles Wilder)於1978年首次引入,它通常與另外兩個相關的指標,即DI+(Positive Directional Indicator)和DI-(Negative Directional Indicator)一起使用。以下是對ADX指標的詳細解釋:

ADX指標的構成: ADX指標主要由以下幾個部分組成:

  1. DI+(Positive Directional Indicator): DI+用於測量正價格移動的強度和方向。它基於價格的正向變化量和總變化量來計算,然後用百分比來表示正向變化的比率。DI+的計算方式如下:

    DI+ = (今日最高價 - 昨日最高價) / 今日最高價與昨日最高價之差 * 100

  2. DI-(Negative Directional Indicator): DI-用於測量負價格移動的強度和方向。它類似於DI+,但是針對價格的負向變化量進行計算。DI-的計算方式如下:

    DI- = (昨日最低價 - 今日最低價) / 昨日最低價與今日最低價之差 * 100

  3. DX(Directional Movement Index): DX是計算DI+和DI-之間的相對關係的指標,用於確定價格趨勢的方向。DX的計算方式如下:

    DX = |(DI+ - DI-)| / (DI+ + DI-) * 100

  4. ADX(Average Directional Index): ADX是DX的平滑移動平均線,通常使用14個交易日的EMA來計算。ADX的計算方式如下:

    ADX = 14日EMA(DX)

ADX的應用: ADX指標可以用於以下幾個方面的技術分析:

  1. 趨勢強度: ADX可以幫助交易者確定價格趨勢的強度。當ADX值高於某一閾值(通常為25或30)時,表示價格趨勢強勁。較高的ADX值可能意味著趨勢可能會持續。反之,ADX值低於閾值時,表示價格可能處於橫盤或弱勢市場中。

  2. 趨勢方向: 當DI+高於DI-時,表示市場可能處於上升趨勢。當DI-高於DI+時,表示市場可能處於下降趨勢。ADX的方向可以幫助確定趨勢的方向。

  3. 背離(Divergence): 當價格趨勢與ADX指標出現背離時,可能表示趨勢的強度正在減弱,這可能是趨勢反轉的信號。

需要注意的是,ADX指標主要用於衡量趨勢的強度和方向,而不是價格的絕對水平。它通常需要與其他技術指標和分析方法結合使用,以提供更全面的市場信息。ADX的參數(如時間週期和閾值)可以根據具體情況進行調整,以滿足不同的交易策略和市場條件。

27.5 :ROC指標(Rate of Change)

ROC(Rate of Change)指標是一種用於技術分析的動量指標,用於衡量資產價格的百分比變化率。ROC指標的主要目的是幫助交易者識別價格趨勢的加速或減速,以及潛在的超買和超賣情況。以下是對ROC指標的詳細解釋:

ROC指標的計算: ROC指標的計算非常簡單,它通常基於某一時間週期內的價格變化。計算ROC的一般步驟如下:

  1. 選擇一個特定的時間週期(例如,14個交易日)。

  2. 計算當前時刻的價格與過去一段時間內的價格之間的百分比變化率。計算公式如下:

    ROC = (當前價格 - 過去一段時間內的價格) / 過去一段時間內的價格 * 100

    過去一段時間內的價格可以是開盤價、收盤價或任何其他價格。

  3. 最終得到的ROC值表示了在給定時間週期內價格的變化率,通常以百分比形式表示。

ROC的應用: ROC指標可以用於以下幾個方面的技術分析:

  1. 趨勢識別: ROC可以幫助交易者識別價格趨勢的加速或減速。當ROC值處於正數區域時,表示價格上漲的速度較快;當ROC值處於負數區域時,表示價格下跌的速度較快。趨勢的加速通常被視為買入信號或賣出信號,具體取決於市場情況。

  2. 超買和超賣情況: ROC指標也可以用來識別資產的超買和超賣情況。當ROC值迅速上升並達到較高水平時,可能表示市場處於超買狀態,價格可能會下跌。相反,當ROC值迅速下降並達到較低水平時,可能表示市場處於超賣狀態,價格可能會上漲。

  3. 背離(Divergence): 當價格走勢與ROC指標出現背離時,可能表示趨勢的弱化或反轉。例如,如果價格創下新高而ROC值沒有創新高,這可能是價格反轉的信號。

需要注意的是,ROC指標通常需要與其他技術指標和市場分析方法結合使用,以提供更全面的市場信息。ROC的參數(如時間週期)可以根據具體情況進行調整,以滿足不同的交易策略和市場條件。

27.6:CCI指標(Commodity Channel Index)

CCI(Commodity Channel Index)是一種常用於技術分析的指標,旨在幫助交易者識別資產價格是否超買或超賣,以及趨勢的變化。CCI指標最初是由唐納德·蘭伯特(Donald Lambert)在20世紀80年代為商品市場設計的,但後來也廣泛用於其他金融市場的技術分析。以下是對CCI指標的詳細解釋:

CCI指標的計算: CCI指標的計算涉及以下幾個步驟:

  1. 計算Typical Price(典型價格): 典型價格是每個交易日的最高價、最低價和收盤價的均值。計算典型價格的公式如下:

    典型價格 = (最高價 + 最低價 + 收盤價) / 3

  2. 計算平均典型價格(平均價): 平均典型價格是過去一段時間內的典型價格的簡單移動平均值。通常,使用一個特定的時間週期(例如20個交易日)來計算平均典型價格。

  3. 計算平均絕對偏差(Mean Absolute Deviation): 平均絕對偏差是每個交易日的典型價格與平均典型價格之間的差的絕對值的平均值。計算平均絕對偏差的公式如下:

    平均絕對偏差 = 平均值(|典型價格 - 平均典型價格|)

  4. 計算CCI指標: CCI指標的計算使用平均絕對偏差和一個常數倍數(通常為0.015)來計算。計算CCI的公式如下:

    CCI = (典型價格 - 平均典型價格) / (0.015 * 平均絕對偏差)

CCI的應用: CCI指標可以用於以下幾個方面的技術分析:

  1. 超買和超賣情況: CCI指標通常在一個範圍內波動,正值表示資產價格相對較高,負值表示價格相對較低。當CCI值大於100時,可能表示市場超買,價格可能會下跌。當CCI值小於-100時,可能表示市場超賣,價格可能會上漲。

  2. 趨勢確認: CCI指標也可以用於確認價格趨勢。當CCI持續保持正值時,可能表示上升趨勢;當CCI持續保持負值時,可能表示下降趨勢。

  3. 背離(Divergence): 當CCI指標與價格圖形出現背離時,可能表示趨勢的弱化或反轉。例如,如果價格創下新高而CCI沒有創新高,這可能是價格反轉的信號。

需要注意的是,CCI指標通常需要與其他技術指標和市場分析方法結合使用,以提供更全面的市場信息。CCI的參數(如時間週期和常數倍數)可以根據具體情況進行調整,以滿足不同的交易策略和市場條件。

27.7:Fibonacci回調和擴展水平

Fibonacci回調和擴展水平是一種基於黃金比例和斐波那契數列的技術分析工具,用於預測資產價格的支撐和阻力水平,以及可能的價格反轉點。這些水平是根據斐波那契數列中的特定比率來計算的。以下是對Fibonacci回調和擴展水平的詳細解釋:

1. Fibonacci回調水平:

  • 0%水平: 這是價格上漲或下跌前的起始點。它代表了沒有任何價格變化的水平。

  • 23.6%水平: 這是最小的Fibonacci回調水平,通常用於標識價格回調的起始點。在上升趨勢中,價格可能在達到一定高度後回調至此水平。在下降趨勢中,價格可能在達到一定低點後回調至此水平。

  • 38.2%水平: 這是另一個重要的Fibonacci回調水平,通常用於識別更深的回調。在趨勢中,價格可能在達到高點或低點後回調至此水平。

  • 50%水平: 這不是斐波那契數列的一部分,但它在技術分析中仍然常常被視為重要水平。價格回調至50%水平通常表示一種中性或平衡狀態。

  • 61.8%水平: 這是最常用的Fibonacci回調水平之一,通常用於標識較深的回調。在趨勢中,價格可能在達到高點或低點後回調至此水平。

  • 76.4%水平: 這是另一個較深的回調水平,有時被用作支撐或阻力水平。

2. Fibonacci擴展水平:

  • 100%水平: 這是價格的起始點,與0%水平相對應。在技術分析中,價格達到100%水平通常表示可能出現完全的價格反轉。

  • 123.6%水平: 這是用於標識較深的價格反轉點的擴展水平。在趨勢中,價格可能在達到一定高度後反轉至此水平。

  • 138.2%水平: 這是另一個擴展水平,通常用於識別更深的價格反轉。

  • 161.8%水平: 這是最常用的Fibonacci擴展水平之一,通常用於標識較深的價格反轉點。

  • 200%水平: 這是價格的終點,與0%水平相對應。在技術分析中,價格達到200%水平通常表示可能出現完全的價格反轉。

Fibonacci回調和擴展水平可以幫助交易者識別可能的支撐和阻力水平,以及價格反轉的潛在點。然而,需要注意的是,這些水平並不是絕對的,不能單獨用於決策。它們通常需要與其他技術指標和分析方法結合使用,以提供更全面的市場信息。此外,市場中的價格行為可能會受到多種因素的影響,因此仍需要謹慎分析。

27.8:均線交叉策略

均線交叉策略是一種常用於技術分析和股票交易的簡單但有效的策略。該策略利用不同時間週期的移動平均線的交叉來識別買入和賣出信號。以下是對均線交叉策略的詳細解釋:

1. 移動平均線(Moving Averages): 均線交叉策略的核心是使用移動平均線,通常包括以下兩種類型:

  • 短期移動平均線(Short-term Moving Average): 通常使用較短的時間週期,如10天或20天,用來反映較短期的價格趨勢。

  • 長期移動平均線(Long-term Moving Average): 通常使用較長的時間週期,如50天或200天,用來反映較長期的價格趨勢。

2. 買入信號: 均線交叉策略的買入信號通常發生在短期移動平均線向上穿越長期移動平均線時,這被稱為“黃金交叉”。這意味著短期趨勢正在上升,可能是買入的好時機。

3. 賣出信號: 均線交叉策略的賣出信號通常發生在短期移動平均線向下穿越長期移動平均線時,這被稱為“死亡交叉”。這意味著短期趨勢正在下降,可能是賣出的好時機。

4. 確認信號: 一些交易者使用其他技術指標或價格模式來確認均線交叉信號的有效性。例如,他們可能會查看相對強度指標(RSI)或MACD指標,以確保市場處於趨勢狀態。

5. 風險管理: 在執行均線交叉策略時,風險管理非常重要。交易者通常會設定止損和止盈水平,以控制風險並保護利潤。止損水平通常設置在買入價格下方,而止盈水平則根據市場條件和交易者的目標而定。

6. 適用性: 均線交叉策略適用於不同市場和資產,包括股票、外匯、期貨和加密貨幣。然而,它可能在不同市場環境下表現不同,因此需要根據市場情況進行調整。

7. 缺點: 均線交叉策略有時會產生虛假信號,特別是在市場處於橫盤或震盪狀態時。因此,交易者需要謹慎使用,並結合其他指標和分析方法來提高準確性。

總之,均線交叉策略是一種簡單但常用的技術分析策略,用於識別買入和賣出信號。它可以作為交易決策的起點,但交易者需要謹慎使用,並結合其他因素來進行綜合分析和風險管理。

27.9: Ichimoku雲

img_1.png Ichimoku雲,也稱為一目均衡圖,是一種綜合性的技術分析工具,最初由日本分析師兼記者一目山人(Goichi Hosoda)在20世紀20年代開發。該工具旨在提供有關資產價格趨勢、支撐和阻力水平以及未來價格走勢的綜合信息。Ichimoku雲由多個組成部分組成,以下是對每個組成部分的詳細解釋:

1. 轉換線(転換線 Tenkan-sen): 轉換線是計算Ichimoku雲的第一個組成部分,通常表示為紅色線。它是最近9個交易日的最高價和最低價的平均值。轉換線用於提供近期價格走勢的參考。

2. 基準線(基準線 Kijun-sen): 基準線是計算Ichimoku雲的第二個組成部分,通常表示為藍色線。它是最近26個交易日的最高價和最低價的平均值。基準線用於提供中期價格走勢的參考。

3. 雲層(先行スパン Senkou Span/Kumo): 雲層是Ichimoku雲的主要組成部分之一,包括兩條線,分別稱為Senkou Span A和Senkou Span B。Senkou Span A通常表示為淺綠色線,是轉換線和基準線的平均值,向前移動26個交易日。Senkou Span B通常表示為深綠色線,是最近52個交易日的最高價和最低價的平均值,向前移動26個交易日。雲層的顏色表示價格走勢的方向,例如,雲層由淺綠色變為深綠色可能表示上升趨勢。

4. 未來雲(Future Cloud): 未來雲是Ichimoku雲中的一部分,通常由兩個Senkou Span線組成,即Senkou Span A和Senkou Span B。未來雲的顏色也表示價格走勢的方向,可以用來預測未來價格趨勢。雲層和未來雲之間的區域稱為“雲中”也叫雲 kumo (抵抗帯 teikoutai ),可以用來識別支撐和阻力水平。

5. 延遲線(遅行スパン Chikou Span): 延遲線是Ichimoku雲的最後一個組成部分,通常表示為橙色線。它是當前收盤價移動到過去26個交易日的線。延遲線用於提供價格走勢的確認,當延遲線在雲層或未來雲之上時,可能表示上升趨勢,當它在雲層或未來雲之下時,可能表示下降趨勢。

Ichimoku雲的主要應用包括:

  • 識別趨勢:Ichimoku雲可以幫助交易者識別價格的長期和中期趨勢。上升趨勢通常表現為雲層由淺綠色變為深綠色,而下降趨勢則相反。

  • 支撐和阻力:雲層和未來雲中的區域可以用作支撐和阻力水平的參考。

  • 買賣信號:均線的交叉以及價格與雲層的相對位置可以提供買入和賣出信號。

需要注意的是,Ichimoku雲是一種複雜的工具,通常需要深入學習和理解。交易者應該謹慎使用,並結合其他技術指標和市場分析方法來進行綜合分析。

27.10:威廉指標(Williams %R)

威廉指標(Williams %R),也稱為威廉超買超賣指標,是一種用於衡量市場超買和超賣情況的動量振盪指標。它是由拉里·威廉斯(Larry Williams)在20世紀70年代開發的。威廉指標的主要目標是幫助交易者識別價格反轉點,並提供買入和賣出的時機。

以下是威廉指標的詳細解釋:

  1. 計算方式: 威廉指標的計算基於以下公式:

    威廉%R = [(最高價 - 當前收盤價) / (最高價 - 最低價)] * (-100)

    • 最高價是在一定時間內的最高價格。
    • 最低價是在一定時間內的最低價格。
    • 當前收盤價是當前週期的收盤價格。

    威廉%R的值通常在-100到0之間波動,其中-100表示市場處於最超賣狀態,0表示市場處於最超買狀態。

  2. 超買和超賣情況: 威廉指標的主要應用是識別市場的超買和超賣情況。當威廉%R的值位於-80或更高時,通常被認為市場處於超賣狀態,可能會發生價格上漲的機會。相反,當威廉%R的值位於-20或更低時,通常被認為市場處於超買狀態,可能會發生價格下跌的機會。

  3. 買入和賣出信號: 威廉指標的買入和賣出信號通常基於以下條件:

    • 買入信號:當威廉%R的值從超賣區域向上穿越-20時,產生買入信號。這表示市場可能正在從超賣狀態中反彈,並可能迎來價格上漲。

    • 賣出信號:當威廉%R的值從超買區域向下穿越-80時,產生賣出信號。這表示市場可能正在從超買狀態中回調,並可能迎來價格下跌。

  4. 背離(Divergence): 交易者還可以使用威廉指標與價格圖形之間的背離來確認信號。例如,如果價格創下新高而威廉%R沒有創新高,這可能表示價格反轉的信號。

  5. 適用性: 威廉指標適用於各種市場,包括股票、外匯、期貨和加密貨幣。然而,需要注意的是,它在不同市場環境下表現可能不同,因此交易者應該謹慎使用,並結合其他技術指標和分析方法來提高準確性。

需要強調的是,威廉指標是一種動量振盪指標,通常用於短期交易。交易者應該將其與其他分析工具和風險管理策略結合使用,以作出更明智的交易決策。

27.11:均幅指標(Average Directional Movement Index,ADX)

均幅指標(Average Directional Movement Index,ADX)是一種用於衡量市場趨勢強度和方向的技術指標。它是由威爾斯·威爾德(Welles Wilder)在1978年首次引入,並在他的著作《新概念技術分析》中詳細描述。ADX的主要用途是幫助交易者確認是否存在趨勢並評估趨勢的強度。以下是對ADX的詳細解釋:

  1. 計算方式: ADX的計算基於一系列的步驟:

    a. 真實範圍(True Range): 首先,需要計算每個週期的真實範圍。真實範圍是以下三個值中的最大值:

    • 當前週期的最高價與最低價之差。
    • 當前週期的最高價與前一個週期的收盤價之差的絕對值。
    • 當前週期的最低價與前一個週期的收盤價之差的絕對值。

    b. 方向定向運動(Directional Movement): 接下來,需要計算正方向定向運動(+DI)和負方向定向運動(-DI)。這些值用於測量上升和下降的趨勢方向。+DI表示上升趨勢方向,而-DI表示下降趨勢方向。

    c. 方向定向運動指數(Directional Movement Index,DX): DX是+DI和-DI之間的差值的絕對值除以它們的和的百分比。

    d. 平均方向定向運動指數(Average Directional Movement Index,ADX): 最後,ADX是DX的移動平均線,通常使用14個週期的簡單移動平均線。

  2. ADX的取值範圍: ADX的值通常在0到100之間,表示市場趨勢的強度。一般來說,ADX的值越高,趨勢越強。當ADX的值高於25到30時,通常被視為趨勢強度足夠,可以考慮進行趨勢跟隨交易。當ADX的值低於25到20時,通常被視為市場處於非趨勢狀態,可能更適合進行區間交易或避免交易。

  3. ADX的應用: ADX可以用於以下方式:

    • 確認趨勢: ADX可以幫助交易者確認市場是否處於趨勢狀態。當ADX的值升高時,表示市場可能處於強烈的趨勢中,可以考慮跟隨趨勢交易。反之,當ADX的值低時,市場可能處於震盪或橫盤狀態。

    • 評估趨勢強度: ADX的值可以用來評估趨勢的強度。較高的ADX值表示趨勢更強烈,而較低的ADX值表示趨勢較弱。

    • 確定交易策略: 交易者可以將ADX與其他技術指標結合使用,例如移動平均線或相對強度指標(RSI),來制定交易策略。

需要注意的是,ADX是一個延遲指標,因為它是基於一定週期的數據計算的。交易者應該將ADX與其他分析工具和風險管理策略一起使用,以作出明智的交易決策。

27.12:多重時間框架分析(Multiple Time Frame Analysis)

多重時間框架分析(Multiple Time Frame Analysis)是一種廣泛用於技術分析和交易決策的方法。它的基本理念是,在進行技術分析時,不僅要考慮單一的時間框架(例如日線圖或小時圖),而是要同時考慮多個不同時間週期的圖表,以獲得更全面的市場信息和更可靠的交易信號。多重時間框架分析有助於交易者更好地瞭解市場的大趨勢、中期趨勢和短期趨勢,以便更明智地做出交易決策。

以下是多重時間框架分析的詳細解釋:

  1. 選擇多個時間框架: 首先,交易者需要選擇多個不同的時間框架來分析市場。通常,會選擇長期、中期和短期時間框架,如日線圖(長期)、4小時圖(中期)和1小時圖(短期)。

  2. 分析長期趨勢: 在最長時間框架上,交易者將查看市場的長期趨勢。這有助於確定市場的主要趨勢方向,例如是否是上升、下降或橫盤。長期趨勢分析通常涉及到趨勢線、移動平均線和其他長期指標的使用。

  3. 分析中期趨勢: 在中期時間框架上,交易者將更詳細地研究市場的中期趨勢。這有助於確定長期趨勢中的次要波動。中期趨勢通常以幾天到幾周為單位。交易者可以使用各種技術工具,如MACD(移動平均收斂散度)或RSI(相對強弱指標)來分析中期趨勢。

  4. 分析短期趨勢: 在短期時間框架上,交易者將更仔細地觀察市場的短期波動。這有助於確定在中期和長期趨勢中的適當入場和出場點。短期趨勢通常以幾小時到幾天為單位。在這個時間框架上,交易者可能使用技術分析中的各種圖形和信號,如頭肩頂和雙底,以及短期移動平均線。

  5. 協調分析結果: 最後,交易者需要協調不同時間框架的分析結果。例如,如果長期趨勢是上升的,中期趨勢也是上升的,那麼短期內出現的下跌可能只是短期波動,而不是反轉趨勢的信號。這種協調有助於避免錯誤的交易決策。

多重時間框架分析的優勢在於它提供了更全面的市場視角,有助於降低交易者因短期波動而做出的錯誤決策的風險。然而,這也需要更多的時間和分析工作,因此需要交易者有耐心和技術分析的知識。

最後,多重時間框架分析不是一種絕對的成功方法,而是一種幫助交易者更好地理解市場的工具。成功的交易還依賴於風險管理、資金管理和心理控制等其他因素。

27.13 指標的遴選和應用

有這麼多判斷超賣超買的指標,到底該怎麼選擇呢?選擇哪種指標來判斷超買和超賣情況,以及其他技術分析工具,取決於你的個人偏好、交易策略和市場狀況。以下是一些建議,幫助你在使用這些指標時作出明智的選擇:

  1. 瞭解不同指標的原理和計算方法: 首先,你應該深入瞭解每個指標的工作原理、計算方式以及它們所衡量的市場特徵。這將幫助你更好地理解它們在不同市場情況下的適用性。

  2. 根據交易策略選擇指標: 你的交易策略應該是決定使用哪些指標的關鍵因素。不同的策略可能需要不同類型的指標。例如,日內交易者可能更關心短期波動,而長期投資者可能更關心趨勢的長期方向。

  3. 多指標確認: 通常,不應該依賴單一指標來做出決策。相反,使用多個指標來確認信號,可以提高你的決策的可靠性。例如,當多個指標同時顯示超買信號時,這可能更具說服力。

  4. 瞭解市場條件: 不同的市場條件下,不同的指標可能更有效。在平靜的市場中,可能更容易出現超買或超賣情況,而在趨勢明顯的市場中,其他趨勢跟蹤指標可能更有用。

  5. 適應時間週期: 選擇指標時,要考慮你所交易的時間週期。某些指標可能在較短時間框架上更為有效,而其他指標可能在較長時間框架上更為有效。

  6. 實踐和回測: 在真實市場之前,先在模擬環境中使用不同的指標進行回測和實踐。這可以幫助你瞭解不同指標的表現,並找到最適合你的策略的指標組合。

  7. 風險管理: 無論你選擇哪些指標,都要記住風險管理的重要性。不要僅僅依賴指標來做出決策,而是將其作為整個交易計劃的一部分。

最終,選擇哪些指標是一項個人化的決策,需要基於你的交易目標、風險承受能力和市場條件做出。建議與其他經驗豐富的交易者交流,學習他們的方法,並根據自己的經驗不斷優化你的交易策略。

判斷這些指標在回測中的表現需要進行系統性的分析和評估。以下是一些步驟,未來會幫助我們來評估指標在回測中的表現:

  1. 選擇回測平臺和數據源: 首先,選擇一個可信賴的回測平臺或軟件,並獲取高質量的歷史市場數據。確保我們的回測環境與實際交易條件儘可能一致。
  2. 制定明確的交易策略: 在回測之前,明確定義我們的交易策略,包括入場規則、出場規則、止損和止盈策略,以及資本管理規則。確保策略清晰且可操作。
  3. 回測參數設置: 針對每個指標,設置適當的參數值。例如,對於RSI,我們可以測試不同的週期(通常是14天),並確定哪個週期在歷史數據上表現最好。
  4. 回測時間段: 選擇一個適當的回測時間段,可以是幾年或更長時間的歷史數據。確保涵蓋不同市場情況,包括趨勢市和橫盤市。
  5. 執行回測: 使用所選的回測平臺執行回測,根據我們的策略和參數值生成交易信號,並模擬實際交易。記錄每筆交易的入場和出場價格、止損和止盈水平,以及交易成本(如手續費和滑點)。
  6. 績效度量: 評估回測的績效。常見的績效度量包括:
    • 累積回報率(Cumulative Returns): 查看策略的總回報。
    • 勝率(Win Rate): 計算獲利交易的比例。
    • 最大回撤(Maximum Drawdown): 識別策略在最差情況下可能遭受的損失。
    • 夏普比率(Sharpe Ratio): 衡量每單位風險所產生的回報。
    • 年化回報率(Annualized Returns): 將回報率 annualize 為年度水平。
  7. 優化參數: 如果回測結果不理想,可以嘗試不同的參數組合或修改策略規則,然後重新進行回測,以尋找更好的表現。
  8. 風險管理: 在回測中也要考慮風險管理策略,如止損和止盈水平的設置,以及頭寸規模的管理。
  9. 實時模擬測試: 最後,在回測表現良好後,進行實時模擬測試以驗證策略在實際市場條件下的表現。

不過最好還是要有這個意識——回測是一種有限制的模擬,不能保證未來表現與歷史表現相同。市場條件會不斷變化,因此,我建議我們應該將回測作為策略開發的一部分,而不是最終的唯一決策依據。此外,在未來我們要持續注意避免他和7th過度擬合(過度優化)的問題,不要過於依賴特定的參數組合,而是尋找穩健的策略。最好的方法是持續監測和優化我們的交易策略,以適應不斷變化的市場。

Upcoming Chapters

Chapter 28 - 引擎系統

Chapter 29 - 日誌系統

Chapter 30 - 投資組合管理

Chapter 31 - 量化計量經濟學

Chapter 32 - 限價指令簿

Chapter 33 - 最優配置和執行

Chapter 34 - 風險控制策略

Chapter 35 - 機器學習