2026-03-12

TA-Lib の Streaming API

2001 年にリリースされた TA-Lib, Technical Analysis Library は、150 以上のテクニカル指標をサポートしていて、長年にわたる検証を経ています。現在の Python 用パッケージは、Cython と Numpy を用いて効率的かつクリーンに TA-Lib をバインドしています。TA-Lib は BSD License (BSD-2-Clause license) の元で配布されているオープンソースのライブラリです。

株価の値動きなどを分析する際に TA-Lib のパッケージを利用していますが、デイトレ用のリアルタイム・システムで利用しようとすると、ちょっと扱いにくいと感じていました。それは、TA-Lib では全てのデータ列に対して計算するからです。そのため、リアルタイムの処理では専用の処理をするクラスを用意して対応しているのですが、計算負荷を少なくするための検討は結構面倒な作業です。

Github の TA-Lib / ta-lib-python プロジェクトサイトにある README をよく読むと、実験段階ながら Streaming API が提供されていることがわかりました。

詳細なベンチマークテストをしていませんが(下に追記しました [2026-03-12])、リアルタイム風に動作するサンプルを作ったので紹介します。

下記の OS 環境で動作確認をしています。

Fedora Linux 43 x86_64
Workstation Edition
Python 3.14.3
numpy 2.4.3
pandas 3.0.1
pyqtgraph 0.14.0
pyside6 6.10.2
ta-lib 0.6.8

TA-Lib の Streaming API とは

従来の API は、過去データ全体に対してインジケーターを計算する設計です。

import numpy as np
import talib as ta

# 過去のデータ全体を一度に処理
prices = np.array([100, 101, 102, 103, 104, 105], dtype=float)
ma = ta.SMA(prices, timeperiod=3)
# 結果:
print(ma)
[ nan  nan 101. 102. 103. 104.]

Streaming API は、直近のデータウィンドウに対してインジケーターを計算します。

import numpy as np
from talib import stream

# 新しいデータポイントが到着するたびに計算
buffer = np.array([100, 101, 102], dtype=float)
ma = stream.SMA(buffer, timeperiod=3)
print(ma)

# 次のデータが到着
buffer = np.array([101, 102, 103], dtype=float)
ma = stream.SMA(buffer, timeperiod=3)
print(ma)
101.0
102.0

Streaming API の主な特徴

  1. 固定長バッファで動作
    • 計算に必要な期間(例:30 期間)のデータのみを保持
    • メモリ効率が良い
  2. 最新値のみを返す
    • 従来の API は配列全体を返すが、Streaming API は単一の値を返す
      • Streaming API は、渡された配列全体に対してインジケーターを計算し、最後の値だけを返す関数です。つまり 1000 個のデータを渡せば 1000 個分の計算をしてしまいます。
      • そのため、プログラム側で固定長バッファを管理する必要があります。
    • リアルタイムモニタリングに最適
  3. 実験的機能 (Experimental)
    • まだ開発中の機能なので、将来的に仕様が変更される可能性がある
    • プロダクション環境では慎重に使用すること

サンプル

ティックデータから単純な移動平均 MA (n=30) を算出して、株価トレンドと一緒にプロットするサンプルです。チャート作成には PyQtGraph を利用しています。

ツールバーにある ▶(再生ボタン)をクリックすると過去のティックデータ(下記サンプル)を読み込み、100 msec 間隔で新しい点を繋げてプロットします。MA を算出できるようになったら緑線で表示されるようになります。

放っておけばデータの最後までプロットしますが、途中で止める場合は、ツールバーにある ⏹(停止ボタン)をクリックします。

sample_data.zip サンプルプログラムが読み込むティックデータ

※ このティックデータは、楽天証券のマーケットスピード2 RSS 経由で自作アプリが収集した 2 秒間隔のティックデータです。

上記のサンプルデータを zip のままで、下記サンプルと同じディレクトリ内に保存してサンプルを実行してください。

import sys
from typing import Optional

import numpy as np
import pandas as pd
import pyqtgraph as pg
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMainWindow, QStyle, QToolBar, QToolButton
from talib import stream


class SampleChart(pg.PlotWidget):
    def __init__(self, ma_period: int = 30) -> None:
        super().__init__()
        self.ma_period = ma_period

        # リストで保持(append が高速)
        self.data_x: list[float] = []
        self.data_y: list[float] = []
        self.data_ma: list[float] = []

        # streaming APIの状態を保持するためのバッファ
        self.y_buffer = np.array([], dtype=float)

        self.line: pg.PlotDataItem = self.plot([], [], pen=pg.mkPen(width=0.5))
        self.ma: pg.PlotDataItem = self.plot([], [], pen=pg.mkPen((0, 255, 0, 192), width=1))

    def add_point(self, x: float, y: float) -> None:
        """新しいデータポイントを追加"""
        self.data_x.append(x)
        self.data_y.append(y)

        # バッファを更新(直近ma_period個のデータのみ保持)
        self.y_buffer = np.append(self.y_buffer, y)
        if len(self.y_buffer) > self.ma_period:
            self.y_buffer = self.y_buffer[-self.ma_period:]

        # streaming APIで移動平均を計算
        if len(self.y_buffer) >= self.ma_period:
            self.data_ma.append(stream.SMA(self.y_buffer, timeperiod=self.ma_period))

        # グラフを更新
        self.line.setData(self.data_x, self.data_y)  # type: ignore

        # MA期間に達したらMAラインを表示
        if len(self.data_ma) > 0:
            ma_start = self.ma_period - 1
            self.ma.setData(self.data_x[ma_start:], self.data_ma)  # type: ignore


class SampleTaLib(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.file_csv = "sample_data.zip"  # ZIP圧縮されたCSVファイルを読み込む
        self.df: Optional[pd.DataFrame] = None
        self.row: int = 0

        self.setWindowTitle("TA-Lib Streaming API Demo")
        self.resize(800, 600)

        # ツールバーの設定
        toolbar = QToolBar()

        but_play = QToolButton()
        but_play.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay))
        but_play.clicked.connect(self.on_play_clicked)
        toolbar.addWidget(but_play)

        but_stop = QToolButton()
        but_stop.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaStop))
        but_stop.clicked.connect(self.on_stop_clicked)
        toolbar.addWidget(but_stop)

        self.addToolBar(toolbar)

        # チャートの設定
        self.chart = SampleChart(ma_period=30)
        self.setCentralWidget(self.chart)

        # タイマーの設定
        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.set_new_data)

    def on_play_clicked(self) -> None:
        """再生ボタンクリック時の処理"""
        try:
            # データ未読み込みの場合のみ読み込む
            if self.df is None:
                self.df = pd.read_csv(self.file_csv)
                print(f"CSVファイルを読み込みました: {len(self.df)}行")

                # データの検証
                if len(self.df.columns) < 2:
                    print("エラー: CSVファイルには少なくとも2列必要です")
                    return

            if not self.timer.isActive():
                self.timer.start()
                print("タイマーを開始しました。")

        except FileNotFoundError:
            print(f"エラー: ファイル '{self.file_csv}' が見つかりません")
        except Exception as e:
            print(f"エラー: {e}")

    def on_stop_clicked(self) -> None:
        """停止ボタンクリック時の処理"""
        if self.timer.isActive():
            self.timer.stop()
            print("タイマーを停止しました。")

    def set_new_data(self) -> None:
        """新しいデータをチャートに追加"""
        if self.df is None or self.row >= len(self.df):
            self.on_stop_clicked()
            print("データの最後に到達しました。")
            return

        try:
            x, y = self.df.iloc[self.row, 0], self.df.iloc[self.row, 1]
            self.chart.add_point(float(x), float(y))
            self.row += 1
        except Exception as e:
            print(f"データ追加エラー: {e}")
            self.on_stop_clicked()


def main() -> None:
    app = QApplication(sys.argv)
    win = SampleTaLib()
    win.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
CSVファイルを読み込みました: 9691行
タイマーを開始しました。
タイマーを停止しました。
サンプルの実行例

ベンチマーク 追記 [2026-03-12]

Anthropic Claude にベンチマーク用のコードを作ってもらいました。現行のアプリに採用している移動平均を算出するクラスのコア部分を抽出したクラスと、今回の TA-Lib の Streaming API を適用したクラスで比較しました。

import time
from collections import deque
import numpy as np
from talib import stream

# 現在の実装
class MovingAverage:
    def __init__(self, window_size: int):
        self.window_size = window_size
        self.queue = deque()
        self.running_sum = 0.0
        self.ma = 0.0
        self.prev_ma = 0.0
    
    def update(self, value: float) -> float:
        if len(self.queue) >= self.window_size:
            self.running_sum -= self.queue.popleft()
        self.queue.append(value)
        self.running_sum += value
        self.prev_ma = self.ma
        self.ma = self.running_sum / len(self.queue)
        return self.ma

# TA-Lib版
class MovingAverageTA:
    def __init__(self, window_size: int):
        self.window_size = window_size
        self.buffer = deque(maxlen=window_size)
        self.ma = 0.0
        self.prev_ma = 0.0
    
    def update(self, value: float) -> float:
        self.buffer.append(value)
        if len(self.buffer) >= self.window_size:
            self.prev_ma = self.ma
            buffer_array = np.array(self.buffer, dtype=float)
            self.ma = stream.SMA(buffer_array, timeperiod=self.window_size)
        return self.ma

# ベンチマーク
def benchmark(ma_class, iterations=100000):
    ma = ma_class(30)
    start = time.perf_counter()
    for i in range(iterations):
        ma.update(float(i))
    elapsed = time.perf_counter() - start
    return elapsed

# 実行
time_custom = benchmark(MovingAverage)
time_talib = benchmark(MovingAverageTA)

print(f"現在の実装: {time_custom:.4f}秒")
print(f"TA-Lib版:   {time_talib:.4f}秒")
print(f"速度比:     {time_talib/time_custom:.2f}倍遅い")

デイトレ・アプリを稼働させている Intel N150 搭載の Windows 11 PC 上でのベンチマークの結果は下記のとおりでした。残念ながら Python だけで記述したクラスの方が全然速いという結果になりました。

現在の実装: 0.0263秒
TA-Lib版:   0.5771秒
速度比:     21.98倍遅い

参考サイト

  1. TA-Lib - Technical Analysis Library
  2. TA-Lib/ta-lib: TA-Lib (Core C Library)
  3. TA-Lib/ta-lib-python: Python wrapper for TA-Lib
  4. TA-Lib · PyPI

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-01-24

シンプルな PDF Viewer 〜 PySide6

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。

PySide6 の GUI アプリで PDF ファイルを閲覧するサンプルを Microsoft Copilot に手伝ってもらいながら作成しました。

ここからカスタマイズしたい処理を加えていく予定なのですが、閲覧のための最低限の機能をコンパクトに実装できたので、一旦はサンプルとしてまとめました。

下記の OS 環境で動作確認をしています。

Fedora Linux 43
Workstation Edition x86_64
Python 3.13.11
PySide6 6.10.1
qt_pdfview.py

以下に実行例を示しました。

ツールバーのアイコンは、Qt にビルトイン・アイコンを利用しました。そのため、ダークモードではアイコンが見えなくなってしまう場合があります。

qt_pdfview.py の実行例

参考サイト

  1. PySide6.QtPdf.QPdfDocument - Qt for Python
  2. PySide6.QtPdfWidgets.QPdfView - Qt for Python

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-01-22

【備忘録】xlwings と QThread

xlwings は、Python を使って Excel を直接操作したり、Excel から Python コードを呼び出したりできるライブラリです[BSD ライセンス]。VBA マクロの代替として Python で Excel の自動化(スクリプト作成、マクロ、ユーザー定義関数(UDF))を実現でき、Excel と Python のデータをシームレスに連携させ、双方の得意な部分を組み合わせて強力な自動化・データ処理を可能にします。

今回の目的
  • PySide6 の GUI アプリから Excel を操作しようと、Excel とやり取りをする部分をスレッド化したところ、うまくいかずにハマってしまいました。
  • 生成 AI から「PySide6 × xlwings を使う人が必ず通る“最大の罠”なんだよ」なんて言われていたのに、二度も同じ間違いをしてしまったので、備忘録にまとめました。

下記の OS 環境で動作確認をしています。

Windows 11 Pro 25H2
Excel 2024 MSO
Python 3.13.9
PySide6 6.10.1
xlwings 0.33.20

簡単なサンプル(GUI 無し)

まずは、xlwings を利用した、ごく簡単な GUI 無しのサンプルです。

import xlwings as xw

if __name__ == "__main__":
    wb = xw.Book()  # 新しいワークブックを開く
    sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化
    sheet["A1"].value = "Python から書き込みました。"  # 値の書き込み
「簡単なサンプル」の実行例

GUI サンプル

GUI のプッシュボタンをクリックして、簡単なサンプル(GUI 無し)と同じ処理を実行するサンプルです。

このサンプルは Excel へ文字列を書き込むだけですが、複雑な処理を加えても GUI のイベントループに影響を与えないように、「別スレッド」で Excel へアクセスする処理をするようにしています。

import sys

import xlwings as xw
from PySide6.QtCore import (
    QObject,
    QThread,
    Signal,
    Slot,
)
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
    QApplication,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class ExcelWorker(QObject):
    def __init__(self):
        super().__init__()
        self.sheet = None

    @Slot()
    def initWorker(self):
        wb = xw.Book()  # 新しいワークブックを開く
        self.sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化

    @Slot(str)
    def excel_write(self, msg: str):
        self.sheet["A1"].value = msg  # 値の書き込み


class SampleXlwings(QWidget):
    requestWorkerInit = Signal()
    requestWrite = Signal(str)

    def __init__(self):
        super().__init__()
        # xlwings用スレッド
        self.thread = thread = QThread()
        self.worker = worker = ExcelWorker()
        worker.moveToThread(thread)
        thread.started.connect(self.requestWorkerInit.emit)
        self.requestWorkerInit.connect(worker.initWorker)
        self.requestWrite.connect(worker.excel_write)
        thread.start()
        # GUI
        layout = QVBoxLayout()
        self.setLayout(layout)
        but = QPushButton("新規 Excel へ文字列を書き込む")
        but.clicked.connect(self.request_write)
        layout.addWidget(but)

    def closeEvent(self, event: QCloseEvent):
        if self.thread is not None:
            self.thread.quit()
            self.thread.wait()
        if self.worker is not None:
            self.worker.deleteLater()
            self.worker = None
        event.accept()

    @Slot()
    def request_write(self):
        msg = "Python から書き込みました。"
        self.requestWrite.emit(msg)


def main():
    app = QApplication(sys.argv)
    win = SampleXlwings()
    win.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

なお、PySide6 では @Slot のデコレータを省略しても動作します。

「GUI サンプル」の実行例

GUI サンプルを実行すると、Excel の新しいワークブックが開きます。

新規 Excel へ文字列を書き込む ボタンをクリックすると、「簡単なサンプル」と同じように、文字列が Excel シートに書き込まれます。

注意点

ExcelWorker クラスのコンストラクタ __init__ メソッドで、以下のように xlwings のインスタンスを定義すると動作しません。

class ExcelWorker(QObject):
    def __init__(self):
        super().__init__()
        wb = xw.Book()  # 新しいワークブックを開く
        self.sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化

この場合、下記のようなエラーが出ます。

pywintypes.com_error: (-2147417842, 'アプリケーションは、別のスレッドにマーシャリングされたインターフェイスを呼び出しました。', None, None)

メインスレッドで ExcelWorker のインスタンスを定義するので、ExcelWorker.__init__ も当然、「メインスレッド」で実行されます。

「別スレッド」で Excel とやり取りをしたければ、「別スレッド」を thread.start() で開始した後に、その「別スレッド」内で xlwings のインスタンスを定義する必要があります。

そういうわけで、「別スレッド」が開始されたときに初期化用のメソッド ExcelWorker.initWorker を実行するようにしています。

self.thread = thread = QThread()
self.worker = worker = ExcelWorker()
worker.moveToThread(thread)
thread.started.connect(self.requestWorkerInit.emit)  # 「別スレッド」が開始されればシグナルを発行
self.requestWorkerInit.connect(worker.initWorker)
self.requestWrite.connect(worker.excel_write)
thread.start()  # 「別スレッド」開始
    @Slot()
    def initWorker(self):
        wb = xw.Book()  # 新しいワークブックを開く
        self.sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化

参考サイト

  1. xlwings Documentation
  2. ASCII.jp:COM(Component Object Model)は古い技術だが、いまだに現役 あらためて解説する (1/2) [2023-04-23]

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-01-14

【備忘録】UNIX秒とタイムゾーン 〜 Pandas

Pandas は、Python プログラミング言語向けに開発されたデータ操作・分析用ソフトウェアライブラリです(三条項 BSD ライセンス)。特に数値テーブルや時系列データの操作のためのデータ構造と演算を提供しています。Pandas という名称は、経済計量学における「パネルデータ」という用語に由来しています。また「Pythonデータ分析」という語句をもじったものともされています 。Pandas により、R 言語で利用可能な DataFrame 操作の多くの同等の機能が Python でも利用できるようになりました。Pandas は NumPy を基盤として構築されています。

Wikipedia より引用、翻訳・編集

タイムゾーンにいつも苦しむ UNIX秒

UNIX 時間(エポック秒)は、UTC(協定世界時)の 1970 年 1 月 1 日 0 時 0 分 0 秒からの経過秒数です。本記事では「UNIX秒」と呼ぶことにします。

リアルタイムのデータを扱う時、取得したデータのタイムスタンプを UNIX秒 に直して保持するようにしています。あとになって分析などをする際に表示用に時刻フォーマットをするのですがタイムゾーンが合っていません。そんなときにいつも、その場しのぎの対処をしてしまっています。

分析やチャート作成時にタイムゾーンの情報は必要ないのですが、UNIX秒をタイムゾーンが付いていないローカル時刻表記に戻す時には、一旦はタイムゾーン付きの処理、変換をしてから、タイムゾーンの情報を削除する、という流れが確実です。

Pandas の Series で .dt アクセサを利用すれば一括処理が可能なので、サンプルでは一つの時刻(UNIX秒)しか扱いませんが、わざわざ Series にしてローカル時刻へ変換する処理の流れをまとめました。丁寧に言葉でくどくど説明しても解りにくいので、コードの簡単なコメントで済ませてしまっています。

import time

import pandas as pd

print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print("UNIX秒からローカル時刻へ変換する流れ")
print("UNIX秒 → UTC (tz-aware) → JST (tz-aware) → tz-naive")
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")

print("\nUNIX秒: 現在のUNIX秒を取得")
ts_epoch = time.time()
print(ts_epoch)

print("\nUNIX秒 → tz-naive: Pandas の Timestamp(utc=True 指定無し)→ Series へ ☹")
dt_no_utc = pd.to_datetime(ts_epoch, unit="s")
ser_no_utc = pd.Series([dt_no_utc])
print(ser_no_utc)

print("\n*** .dt アクセサを使った変換[備忘録] ***")
print("UNIX秒 → UTC (tz-aware): Pandas の Timestamp(タイムゾーン付き)→ Series へ")
dt_utc = pd.to_datetime(ts_epoch, unit="s", utc=True)
ser_utc = pd.Series([dt_utc])
print(ser_utc)

print("\nUTC (tz-aware) → JST (tz-aware): タイムゾーンを日本時間に変更")
ser_jst = ser_utc.dt.tz_convert("Asia/Tokyo")
print(ser_jst)

print("\nJST (tz-aware) → tz-naive: タイムゾーン情報を削除")
ser_no_tz = ser_jst.dt.tz_localize(None)
print(ser_no_tz)
実行例
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
UNIX秒からローカル時刻へ変換する流れ
UNIX秒 → UTC (tz-aware) → JST (tz-aware) → tz-naive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

UNIX秒: 現在のUNIX秒を取得
1768363958.6145868

UNIX秒 → tz-naive: Pandas の Timestamp(utc=True 指定無し)→ Series へ ☹
0   2026-01-14 04:12:38.614586830
dtype: datetime64[ns]

*** .dt アクセサを使った変換[備忘録] ***
UNIX秒 → UTC (tz-aware): Pandas の Timestamp(タイムゾーン付き)→ Series へ
0   2026-01-14 04:12:38.614586830+00:00
dtype: datetime64[ns, UTC]

UTC (tz-aware) → JST (tz-aware): タイムゾーンを日本時間に変更
0   2026-01-14 13:12:38.614586830+09:00
dtype: datetime64[ns, Asia/Tokyo]

JST (tz-aware) → tz-naive: タイムゾーン情報を削除
0   2026-01-14 13:12:38.614586830
dtype: datetime64[ns]

参考サイト

  1. pandasで日付・時間の列を処理(文字列変換、年月日抽出など) | note.nkmk.me

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加