ラベル Qt の投稿を表示しています。 すべての投稿を表示
ラベル Qt の投稿を表示しています。 すべての投稿を表示

2026-09-04

QQuickWidget でトレンドチャート 〜 PySide6

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

QChart を含む QtCharts モジュールは、Qt 6.10 / PySide6 6.10 以降、公式に非推奨となりました[1] [2]。

従来 QChart で作成していたプロットを(Matplotlib や PyQtGraph を利用せずに)新しいやり方で作成できたので、前回に引き続き、備忘録的にサンプルを紹介します。

今回は、折れ線グラフです。時間の経過に伴うデータの変化や傾向(トレンド)を表すことを意識して、トレンドチャートと呼んでいます。

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

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.7
PySide6 6.11.2
NumPy 2.5.2

トレンドチャートのサンプル

実行例

まずは、実行例を示します。乱数を生成してつなげてプロットしただけのサンプルです。

qt_quickwidget_trend.py の実行例

Python のコード

ひとつのファイルにまとめたコードを示しました。

import sys

import numpy as np
from PySide6.QtCore import Qt
from PySide6.QtGraphs import (
    QGraphsTheme,
    QLineSeries,
    QValueAxis,
)
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QStyle,
)


class MyTrend(QQuickWidget):
    def __init__(self):
        super().__init__()
        self.setResizeMode(
            QQuickWidget.ResizeMode.SizeRootObjectToView
        )
        self.setFixedSize(600, 300)

        # --------------------------------
        # Theme
        # --------------------------------
        self.theme = theme = QGraphsTheme()
        grid = theme.grid()
        grid.setMainWidth(1.0)
        grid.setSubWidth(0.5)
        theme.setGrid(grid)

        # --------------------------------
        # Axes
        # --------------------------------
        self.axis_x = axis_x = QValueAxis()
        axis_x.setRange(0, 100)

        self.axis_y = axis_y = QValueAxis()
        axis_y.setRange(0, 1)
        axis_y.setTitleText("Value")

        # --------------------------------
        # Line series
        # --------------------------------
        self.series = series = QLineSeries()
        series.setAxisX(axis_x)
        series.setAxisY(axis_y)

        # 線の太さと色
        series.setWidth(1.0)
        series.setColor(Qt.GlobalColor.green)

        # GraphsView に渡すプロパティ
        self.setInitialProperties({
            "theme": theme,
            "axisX": axis_x,
            "axisY": axis_y,
            "seriesList": [series],
        })

        # QtGraphs の GraphsView をロード
        self.loadFromModule(
            "QtGraphs",
            "GraphsView",
        )

    def append_data(self, x: float, y: float):
        self.series.append(x, y)


class Example(QMainWindow):
    def __init__(self):
        super().__init__()

        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMenuButton
        )
        self.setWindowIcon(icon)
        self.setWindowTitle("Trend Chart")

        # --------------------------------
        # QQuickWidget
        # --------------------------------
        trend = MyTrend()
        self.setCentralWidget(trend)

        arr_y = np.random.random(100)
        for i, y in enumerate(arr_y):
            trend.append_data(i, y)


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


if __name__ == "__main__":
    main()

プログラムの流れ

このプログラムでは、QQuickWidget を継承した MyTrend クラスを作成し、Qt Graphs の QLineSeries を使って折れ線グラフを表示しています。MyTrend の初期化時に表示サイズを 600×300 に固定し、グラフのテーマ、グリッド線、X軸とY軸、そして折れ線グラフ用の系列を順番に設定します。

テーマには QGraphsTheme を使用し、グリッド線についてはメインの線を 1 ピクセル、サブの線を 0.5 ピクセルに設定しています。X軸とY軸には QValueAxis を使用し、X軸を 0 ~ 100、Y軸を 0 ~ 1 の範囲に設定しています。また、Y軸には Valueというタイトルを設定しています。

グラフのデータを保持する QLineSeries を作成し、先ほど作成した X軸 と Y軸 を割り当てます。折れ線の太さを 1 ピクセル、色を緑色に設定した後、テーマ、軸、系列を GraphsView に渡すための初期プロパティとして設定します。その後、QtGraphs モジュールの GraphsView をロードすることで、グラフが表示される状態になります。

MyTrend には append_data() メソッドを用意しており、X座標 と Y座標を受け取って QLineSeries に追加します。これによって、グラフそのものを管理する MyTrend と、そこへデータを供給する側を分離しています。


Example クラスでは MyTrend をメインウィンドウの中央ウィジェットとして配置し、NumPy の random.random() を使って 100個 のランダムな値を生成します。生成した値を enumerate() で順番に取り出し、インデックスを X座標、ランダムな値を Y座標 として append_data() に渡します。

その結果、0 ~ 99 の X座標 に対して 0 ~ 1 の範囲でランダムに生成された 100個 のデータが QLineSeries に追加され、Qt Graphs によって折れ線グラフとして描画されます。

最後に main() で QApplication を生成し、メインウィンドウを表示してイベントループを開始します。

参考サイト

  1. Qt Charts C++ Classes | Qt Charts
  2. Obsolete Members for QChart | Qt Charts
  3. PySide6.QtGraphs.QGraphsTheme - Qt for Python
  4. PySide6.QtGraphs.QLineSeries - Qt for Python
  5. PySide6.QtGraphs.QValueAxis - Qt for Python
  6. PySide6.QtQuickWidgets.QQuickWidget - Qt for Python

 

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

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



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

2026-09-03

QQuickWidget で散布図 〜 PySide6

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

QChart を含む QtCharts モジュールは、Qt 6.10 / PySide6 6.10 以降、公式に非推奨となりました[1] [2]。

従来 QChart で作成していたプロットを(Matplotlib や PyQtGraph を利用せずに)新しいやり方で作成できたので、備忘録的にサンプルを紹介します。現時点では自由に使いこなせるほど理解ができているとは言えないので、より良い方法が判れば、その都度、投稿していく予定です。

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

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.7
PySide6 6.11.2
NumPy 2.5.2

散布図のサンプル

実行例

まずは、実行例を示します。乱数を生成して散布図にプロットしただけのサンプルです。

qt_quickwidget_scatter.py の実行例

Python のコード

ひとつのファイルにまとめたコードを示しました。

import sys

import numpy as np
from PySide6.QtCore import QUrl
from PySide6.QtGraphs import (
    QGraphsTheme,
    QScatterSeries,
    QValueAxis,
)
from PySide6.QtQml import QQmlComponent, QQmlEngine
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QStyle,
)


class MyScatter(QQuickWidget):
    def __init__(self):
        super().__init__()
        self.setResizeMode(
            QQuickWidget.ResizeMode.SizeRootObjectToView
        )
        self.setFixedSize(600, 600)

        # --------------------------------
        # Theme
        # --------------------------------
        self.theme = theme = QGraphsTheme()
        grid = theme.grid()
        grid.setMainWidth(1.0)
        grid.setSubWidth(0.5)
        theme.setGrid(grid)

        # --------------------------------
        # Axes
        # --------------------------------
        self.axis_x = axis_x = QValueAxis()
        axis_x.setRange(0, 1)

        self.axis_y = axis_y = QValueAxis()
        axis_y.setRange(0, 1)

        # --------------------------------
        # Scatter series
        # --------------------------------
        self.series = series = QScatterSeries()
        series.setAxisX(axis_x)
        series.setAxisY(axis_y)

        # ----------------------
        # QMLで描画ポイントを修飾
        # ----------------------
        qml = """
        import QtQuick

        Rectangle {
            width: 5
            height: 5
            radius: width / 2
        }
        """
        self.engine = QQmlEngine()
        self.point_delegate = QQmlComponent(self.engine)
        self.point_delegate.setData(qml.encode(), QUrl())

        if self.point_delegate.isError():
            print(self.point_delegate.errors())
        else:
            series.setPointDelegate(self.point_delegate)

        # GraphsView に渡すプロパティ
        self.setInitialProperties({
            "theme": theme,
            "axisX": axis_x,
            "axisY": axis_y,
            "seriesList": [series],
        })

        # QtGraphs の GraphsView をロード
        self.loadFromModule(
            "QtGraphs",
            "GraphsView",
        )

    def append_data(self, x: float, y: float):
        self.series.append(x, y)


class Example(QMainWindow):
    def __init__(self):
        super().__init__()

        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMenuButton
        )
        self.setWindowIcon(icon)
        self.setWindowTitle("Scatter Chart")

        # --------------------------------
        # QQuickWidget
        # --------------------------------
        scatter = MyScatter()
        self.setCentralWidget(scatter)

        arr_xy = np.random.random(size=(200, 2))
        for x, y in arr_xy:
            scatter.append_data(x, y)


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


if __name__ == "__main__":
    main()

プログラムの流れ

まず main() が呼び出され、Qt アプリケーションの実行環境が作られます。その後、Example クラスのウィンドウが生成されます。

Example の初期化では、まずメインウィンドウのアイコンとタイトルを設定します。続いて MyScatter のインスタンスを生成し、メインウィンドウの中央ウィジェットに設定します。

MyScatter は QQuickWidget を継承しています。MyScatter の初期化では、

  1. 最初にウィジェットのリサイズ方法とサイズを設定します。
  2. 次に、グラフ全体の外観を管理する QGraphsTheme を生成します。このテーマからグリッド線の設定を取得し、主グリッド線と補助グリッド線の線幅を変更してから、テーマへ戻します。
  3. 続いて X 軸と Y 軸をそれぞれ QValueAxis として生成し、両方とも 0 から 1 の範囲に設定します。
  4. その後、散布図のデータを保持する QScatterSeries を生成します。ここで X 軸と Y 軸をこのシリーズに関連付けます。
  5. 次に、散布図の各データポイントをどのように描画するかを設定します。今回は QML を外部ファイルにせず、Python の文字列として Rectangle を定義しています。この Rectangle は幅と高さを 5 にし、角を丸めることで円形のマーカーになります。
  6. この QML を実際に利用するために QQmlEngine と QQmlComponent を生成し、QML の文字列を QQmlComponent に読み込ませます。QML にエラーがなければ、そのコンポーネントを QScatterSeries の pointDelegate に設定します。
  7. グラフを構成するオブジェクトの準備が終わったら、テーマ、X 軸、Y 軸、散布図シリーズを GraphsView の初期プロパティとして登録します。
  8. 最後に loadFromModule() を呼び出し、Qt Graphs の GraphsView をロードします。この順序によって、先ほど準備したテーマ、軸、シリーズが GraphsView に初期状態として渡されます。

ここまでで MyScatter の準備が完了します。

Example に戻ると、MyScatter が中央ウィジェットとして配置された後、NumPy を使って 200 個のランダムな X・Y 座標を生成します。それぞれの座標について MyScatter の append_data() を呼び出し、QScatterSeries にデータポイントを追加します。

データが追加されると、Qt Graphs がそれぞれのポイントを描画します。その際、先ほど設定した pointDelegate が使われるため、通常の散布点ではなく、QML の Rectangle によって定義された直径 5 程度の円として表示されます。

最後に Example のウィンドウを表示し、Qt のイベントループを開始します。以降は Qt が画面の描画やイベント処理を担当し、生成された 200 個のデータポイントが散布図として表示されます。

参考サイト

  1. Qt Charts C++ Classes | Qt Charts
  2. Obsolete Members for QChart | Qt Charts
  3. PySide6.QtGraphs.QGraphsTheme - Qt for Python
  4. PySide6.QtGraphs.QScatterSeries - Qt for Python
  5. PySide6.QtGraphs.QValueAxis - Qt for Python
  6. PySide6.QtQml.QQmlComponent - Qt for Python
  7. PySide6.QtQml.QQmlEngine - Qt for Python
  8. PySide6.QtQuickWidgets.QQuickWidget - Qt for Python

 

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

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



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

2026-06-20

モジュールを動的にインポート 〜 Python 〜

Python アプリを作っていると、動的にクラスをインポートしたいニーズがときどきあります。

覚えた機能について今後も使う可能性があれば、その時になってあれこれ調べ直さなくて済むように、簡単なサンプルを作成して本ブログに記事にまとめています。動的にクラスをインポートについても、何年か前に記事を書いていました [1]。しかし、読み直してみると、今ひとつよく解らなかったので、一般的な用途に合うようにサンプルを作り直しました。

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

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.5
PySide6 6.11.1

プラグイン用サンプル

まず、動作確認用にプラグイン用サンプルを用意します。プロジェクト内に plugins というディレクトリを作り、テンプレート用の抽象クラスを記述した abstract.py(PluginTemplate クラス)と、このクラスを継承して実装した simple_1.py と simple_2.py を用意します。

plugins/
├ abstract.py
├ simple_1.py
└ simple_2.py
from abc import ABC, abstractmethod


class PluginTemplate(ABC):
    NAME = "template"

    @abstractmethod
    def run(self) -> None: ...
from plugins.abstract import PluginTemplate


class Plugin(PluginTemplate):
    NAME = "simple 1"

    def run(self) -> None:
        print(self.NAME)
from plugins.abstract import PluginTemplate


class Plugin(PluginTemplate):
    NAME = "simple 2"

    def run(self) -> None:
        print(self.NAME)

PluginTemplate クラスを継承して実装した simple_1.py と simple_2.py のクラス名は、ひとまず Plugin に統一しています。

クラス一覧をインポートする関数

指定したパス、パッケージ名、継承したクラス名に合致するモジュールをロードする関数 load_plugins です。

import importlib
import inspect
import pkgutil


def load_plugins(
        path_plugin: str,
        package_name: str,
        plugin_base_class: type
) -> dict[str, type]:
    # NOTE:
    # 現在はプロジェクト配下のパッケージのみを対象としているため、
    # importlib.import_module() を利用している。
    #
    # 将来的にユーザーが任意のディレクトリへ配置した外部プラグインを
    # 読み込む場合は、sys.path の追加、または
    # importlib.util.spec_from_file_location() を利用した
    # ファイルパスベースのロード方式を検討すること。
    dict_plugin = {}
    for _, module_name, _ in pkgutil.iter_modules([path_plugin]):
        module = importlib.import_module(f"{package_name}.{module_name}")
        for _, cls in inspect.getmembers(module, inspect.isclass):
            if issubclass(cls, plugin_base_class) and cls is not plugin_base_class:
                dict_plugin[cls.NAME] = cls
    return dict_plugin

引数 plugin_base_class に指定した抽象クラス(この例では PluginTemplate)を継承したクラス cls のみを辞書に cls.NAME をキーに登録して返す関数です。

テスト用 GUI サンプル

動的にクラスをインポートするプラグインの機能を確認する PySide6 の GUI サンプルです。

import sys

from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
    QApplication,
    QComboBox,
    QMainWindow,
    QStyle,
    QToolBar,
)

from load_plugins import load_plugins
from plugins.abstract import PluginTemplate


class Example(QMainWindow):
    def __init__(self):
        super().__init__()

        # プラグインの一覧を取得
        plugins: dict[str, type] = load_plugins(
            path_plugin="./plugins",
            package_name="plugins",
            plugin_base_class=PluginTemplate
        )

        self.setWindowTitle("Plugin Sample")

        toolbar = QToolBar()
        self.addToolBar(toolbar)

        self.combo = combo = QComboBox()
        for key in sorted(plugins.keys()):
            cls = plugins[key]
            combo.addItem(key, cls)
        toolbar.addWidget(combo)

        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_MediaPlay
        )
        action_play = QAction(self)
        action_play.setIcon(icon)
        action_play.triggered.connect(self.on_play)
        toolbar.addAction(action_play)

    def on_play(self):
        cls = self.combo.currentData()
        obj = cls()
        obj.run()


def main():
    app = QApplication(sys.argv)
    ex = Example()
    ex.show()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()

サンプルはコンボボックスに動的に読み込んだクラスの NAME の一覧を表示しています。隣の ▶ ボタンをクリックすると、コンボボックスに表示されている NAME に対応するクラスのインスタンスを生成して、run メソッドを実行します。

qt_plugin_sample.py の実行例
simple 1
simple 2

参考サイト

  1. bitWalk's: プラグイン・ウィジェットを扱う ~ PySide6 ~ [2023-02-07]
  2. importlib --- import の実装 — Python ドキュメント
  3. inspect --- 活動中のオブジェクトを調査する — Python ドキュメント
  4. pkgutil --- パッケージ拡張ユーティリティ — Python ドキュメント

 

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

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



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

2026-06-16

NavigationToolbar2QT のカスタマイズ 〜 Matplotlib 〜

Matplotlib は、Python と NumPy のためのプロットライブラリです。Tkinter、wxPython、Qt、GTK のような汎用 GUI ツールキットを使ったアプリケーションにプロットを埋め込むためのオブジェクト指向 API を提供しています。

Wikipedia より引用、翻訳

PySide6 の GUI で Matplotlib のチャートを扱うときに利用する、ズームや保存などの便利な機能を提供するナビゲーション・ツールバー NavigationToolbar2QT について、ユーザー定義のボタンを追加する方法(+α)をまとめました。

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

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.5
matplotlib 3.11.0
pyside6 6.11.1
seaborn 0.13.2

実行例

サンプルとして正規分布の乱数のヒストグラムを表示していますが、今回のテーマは、このチャートの下に表示されているナビゲーション・ツールバーのカスタマイズです。

ナビゲーション・ツールバーのカスタマイズ例として、「保存」ボタンの右側に「Qt」アイコン (SP_TitleBarMenuButton) を表示しています。

qt_matplotlib/toolbar.py の実行例

NavigationToolbar2QT を継承

NavigationToolbar2QT を継承して「Qt」アイコン (SP_TitleBarMenuButton)(ボタン)を追加しています。

from matplotlib.backends.backend_qtagg import (
    NavigationToolbar2QT as NavigationToolbar,
)

class MyNavToolbar(NavigationToolbar):
    def __init__(self, canvas: FigureCanvas):
        super().__init__(canvas)
        user_action = QAction("User", self)
        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMenuButton
        )
        user_action.setIcon(icon)
        user_action.triggered.connect(self.on_user_action)

        actions = self.actions()

        n = len(actions)
        self.insertAction(actions[n - 1], user_action)

    def on_user_action(self):
        # Zoomモードなら解除
        if self._actions["zoom"].isChecked():
            self._actions["zoom"].trigger()
        # Panモードなら解除
        if self._actions["pan"].isChecked():
            self._actions["pan"].trigger()

        print("User button clicked")

「Qt」アイコン (SP_TitleBarMenuButton)(ボタン)をクリックすると、on_user_action メソッドが実行されて文字列が標準出力されますが、その他の機能として、ズームボタン self._actions["zoom"] あるいはパン(移動)ボタン self._actions["pan"] が選択状態であれば解除するようにしています。

サンプル・コード

サンプルコード qt_matplotlib_toolbar.py を示しました。

qt_matplotlib/toolbar.py

参考サイト

  1. Embedding in Qt — Matplotlib documentation

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

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



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

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技術ブログ オープンソースへ
にほんブログ村

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



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