4. Erforschen Sie Querschnitts-Timingfaktoren mit HistoryPanel

Querschnitts-Timing: Faktorschwelle -> Bool-Maske -> Portfoliorenditen und CAGR vergleichen

Dieses Tutorial zielt auf ein sehr häufiges und sehr praktisches Forschungsszenario ab: Wir haben nur eine kleine Anzahl von Aktien (einige bis ein Dutzend oder so) und möchten Timing-Entscheidungen zum Halten/Nicht-Halten entlang der eigenen Zeitachse jeder Aktie treffen; Fassen Sie diese Entscheidungen dann in einer einzigen Portfoliokurve zusammen und vergleichen Sie sie schließlich mit der Benchmark (HS300), um eine wiederverwendbare jährliche Kennzahl (CAGR) zu erhalten.

Lassen Sie uns zunächst die Positionierung klarstellen: „HistoryPanel“ dient hier als Container für die Forschung mit leichtem Faktor. Seine Stärke besteht darin, dass es „Daten -> Bedingungen -> Forschungsdefinition (Maske) -> Portfolio-Aggregation -> visuelle Erklärung“ miteinander verbindet und es uns ermöglicht, schnell zu überprüfen, ob es sich lohnt, eine Regel näher zu untersuchen. Es handelt sich nicht um eine Trading-Backtesting-Engine: Sie verarbeitet nicht die vollständige Handelssemantik wie Transaktionskosten, Abwicklung, Slippage, Kapitalbeschränkungen, Mindesthandelsgröße usw. Das Ziel dieses Artikels ist der Aufbau einer Forschungsschleife, nicht einer Handelsschleife.


4.1. 0. 开场:先跑通一个最小可用的研究闭环

Wie der folgende Code zeigt, können wir mit nur wenigen Zeilen ein „HistoryPanel“ erhalten und das erste Diagramm zeichnen (sei es ein Liniendiagramm oder Candlesticks), was beweist, dass „die Daten- und Visualisierungspipeline funktioniert“.

import qteasy as qt

stocks = ['000001.SZ', '600519.SH', '300750.SZ']
benchmark = '000300.SH'
shares = stocks + [benchmark]

hp = qt.get_kline(
    shares=shares,
    start='20220101',
    end='20221231',
    freq='D',
    as_panel=True,
)

# 最小可跑:能出图即可
fig = hp.plot(interactive=True)
fig

Wenn Sie dies in einem Notebook ausführen, können Sie im obigen Snippet normalerweise sofort ein Diagramm sehen.

Allerdings reicht es bei weitem nicht aus, nur „etwas planen“ zu können. Wenn wir es wirklich als tägliches Recherchetool verwenden wollen, werden wir auf mindestens die folgenden Probleme stoßen:

  1. So implementieren Sie die Timing-Regel: Es ist einfach, sich eine Regel wie „Halten, wenn MACD>0“ vorzustellen, aber sobald man sie als Code schreibt, wird sie oft verzerrt: falsche Spaltennamen, fehlende Spalten, alles kommt im NaN-Format heraus oder verschiedene Aktien sind inkonsistent ausgerichtet. Das Ergebnis ist, dass das Rechercheskript einen Fehler ausgibt, sobald Sie es ausführen – oder schlimmer noch, es macht keinen Fehler, aber die Schlussfolgerungen sind nicht vertrauenswürdig.

  2. So richten Sie Maskenformen aus: Die Bedingung ist im Wesentlichen 2D (Bestand × Zeit), aber „HistoryPanel“ ist 3D (Bestand × Zeit × Feld). Wenn die Formen nicht übereinstimmen, kann es zu einem subtilen Fehler kommen, bei dem man denkt, dass man Aktien filtert, in Wirklichkeit aber Felder filtert. Und diese Art von Fehler löst oft nicht sofort einen Fehler aus, sondern lässt die Eigenkapitalkurve nur „ein bisschen daneben“ aussehen.

  3. Ohne Benchmark ist es schwierig, die Leistung zu beurteilen: Wenn man sich nur die Portfoliokurve anschaut, kann man sich leicht selbst auf die Schulter klopfen. Möglicherweise folgt es in einem Teil des Jahres 2022 einfach einem bestimmten Stil oder einem breiten Markttrend. Die Einbeziehung von „000300.SH“ als Referenz beantwortet zumindest eine praktischere Frage: Erzeugt diese Timing-Regel Überschussrenditen oder bewegt sie sich einfach nur mit dem Markt**?

  4. Ergebnisse, aber schwer zu erklären: Selbst wenn das Long-Portfolio eine Outperformance erzielt, ist es immer noch schwer zu erklären, „warum es gewonnen hat“. Echte Forschung muss reproduzierbar und überprüfbar sein: Wenn die Rendite einen klaren Wendepunkt zeigt, können wir dann schnell zum Diagramm zurückkehren und genau bestimmen, „welche Auslöser den Positionsstatus geändert haben“?

Glücklicherweise können diese Fähigkeiten Schritt für Schritt ausgefüllt werden. Dieser Artikel folgt dem Rhythmus „Erst zum Laufen bringen, dann verbessern“, um daraus einen wirklich brauchbaren kleinen Forschungsworkflow zu entwickeln.


4.2. 0.5 Zeigen Sie zunächst das Endergebnis (was am Ende herauskommt)

Um das Tempo gleichmäßiger zu halten, machen wir zunächst den „Endzustand“ klar. Nachdem Sie diesem Artikel gefolgt sind, erhalten Sie mindestens zwei Arten von Ausgaben:

  1. Vergleich auf Portfolioebene: Platzieren Sie die „LONG/SHORT/000300.SH“-Kurven (alle normalisiert, um bei 1,0 zu beginnen) im selben Diagramm, und Sie können auf einen Blick erkennen, ob „die Timing-Regel während des Forschungszeitraums wirksam ist“.

  2. Erklärung auf Ebene einzelner Vermögenswerte: Zeichnen Sie das Candlestick-Diagramm (oder die Preiskurve) für eine bestimmte Aktie und markieren Sie die Zeitpunkte, zu denen die „Haltebedingung“ ausgelöst wird. Wenn Sie auf diese Weise ein abnormales Segment in der Portfoliokurve entdecken, können Sie schnell zum Einzelaktiendiagramm zurückkehren, um zu überprüfen, ob die „Triggerpunkte“ Ihrer Intuition entsprechen.

Hinweis: Für diesen Artikel ist die Ausgabe eines GIF nicht erforderlich. Ein empfehlenswerterer Ansatz ist: Zuerst die Diagramme in einem Notebook zum Laufen bringen und einfach Screenshots machen; Wenn Sie einen Blogbeitrag schreiben oder eine Präsentation halten möchten, zeichnen Sie die wichtigsten Schritte in einem GIF auf.


4.3. 1. 目标(我们这篇文章要完成什么)

Bevor wir beginnen, klären wir das Ziel, damit wir bei jedem Schritt wissen, welches Problem wir lösen.

  • Rufen Sie „HistoryPanel“ für eine kleine Menge einzelner Aktien + „000300.SH“ ab

  • Leiten Sie einen interpretierbaren Zeitfaktor ab (Beispiel: MACD)

  • Verwenden Sie Vergleichsoperationen, um boolesche Bedingungen zu erhalten, und verwenden Sie „where()“, um die Forschungsmaske zu generieren

  • Verwenden Sie „portfolio(mask=…)“, um eine Portfoliokurve zu aggregieren und diese mit der Benchmark zu vergleichen

  • Verwenden Sie „normalize/cum_return“, um kumulative Renditen zu erhalten und eine CAGR-Zusammenfassung abzuleiten

  • Verwenden Sie „plot(highlight=…)“, um die „Triggerpunkte“ im Diagramm zu erklären

  • Am Ende stellen wir einen vollständigen Code bereit, der „als einzelne Funktion ausgeführt wird“.

  • Klären Sie die Grenzen des Umfangs: Dies ist keine Trading-Backtesting-Engine

Der gesamte Artikel folgt dem gleichen Rhythmus: erklären Sie zunächst, was in diesem Abschnitt gelöst werden soll -> gehen Sie dann auf die minimal notwendigen Prinzipien ein -> geben Sie schließlich den Schlüsselcode und das erwartete Ergebnis an. Wiederholter Code wird ggf. weggelassen, es ist jedoch garantiert, dass jeder Abschnitt reproduzierbar ist, indem der Artikel vollständig durchgelesen wird.


4.4. 1.1 Voraussetzung für die Reproduktion: Sind die Daten bereits lokal aufbereitet?

In den Beispielen in diesem Artikel wird standardmäßig davon ausgegangen, dass Sie die Datenquelle lokal konfiguriert haben und dass „qt.get_kline()“ die täglichen Balkendaten für „20220101“–20221231 erfolgreich abrufen kann.

Wenn Ihre Umgebung noch keine Daten hat, sind die häufigsten Symptome: „qt.get_kline()“ gibt ein leeres Panel zurück, oder Sie erhalten alle NaNs, wenn Sie später Indikatoren berechnen. Um zu vermeiden, dass Sie „erst auf halbem Weg merken, dass keine Daten vorhanden sind“, wird empfohlen, dass Sie nach Abschluss von Abschnitt 2 Folgendes überprüfen:

  • Ob die Zeitdauer von „hp.shape“ größer als 100 ist (ein Jahr mit täglichen Balken beträgt normalerweise etwa 200);

  • Enthält hp.htypes mindestens open/high/low/close;

  • Ob „hp.hdates“ kontinuierlich den Zeitraum abdeckt, den Sie studieren möchten.

Wenn Sie zuerst Daten in Ihrer eigenen Umgebung herunterladen müssen, schließen Sie bitte zuerst die Kapitel „Datendownload und Datenquellenkonfiguration“ ab. Dieser Artikel geht nicht auf Details der Datenpipeline ein und konzentriert sich auf den Forschungsworkflow rund um „HistoryPanel“ selbst.


4.5. 2. 准备数据:三只个股 + 一个基准指数(HS300)

2.1 Was dieser Abschnitt lösen soll

Lassen Sie uns zunächst die Daten bereinigen: Verwenden Sie drei Einzelaktien für das Querschnittstiming und fügen Sie dem Panel außerdem „000300.SH“ (CSI 300) als Benchmark für spätere „Portfolio(…, Benchmark=…)“-Vergleiche hinzu.

Der wichtigste Schritt hier ist: Stellen Sie sicher, dass die OHLC-Säulen vollständig sind. Denn egal, ob Sie Candlesticks zeichnen oder später eine Mustererkennung durchführen, Sie können „Eröffnung/Hoch/Tief/Schluss“ nicht vermeiden.

2.2 Mindestgrundsätze

„HistoryPanel“ sind 3D-Daten: „(share, time, htype)“. Nachfolgendes „where/mask/portfolio/cum_return“ wird sich alle auf die Voraussetzung der „Formausrichtung“ stützen. So at the very beginning we print shape/shares/htypes and do a minimal field validation, which can block most of the “only errors out halfway through writing it” pitfalls in advance.

2.3 Ausführbarer Code + erwartete Ergebnisse

import qteasy as qt

stocks = ['000001.SZ', '600519.SH', '300750.SZ']
benchmark = '000300.SH'
shares = stocks + [benchmark]

hp = qt.get_kline(
    shares=shares,
    start='20220101',
    end='20221231',
    freq='D',
    as_panel=True,
)

print('hp.shape:', hp.shape)
print('hp.shares:', hp.shares)
print('hp.htypes:', hp.htypes)
print('last_date:', hp.hdates[-1])

required = {'open', 'high', 'low', 'close'}
missing = [c for c in required if c not in set(hp.htypes)]
if missing:
    raise ValueError(f'Missing required OHLC columns in htypes: {missing}')
if benchmark not in hp.shares:
    raise ValueError(f'Benchmark {benchmark} not found in shares: {hp.shares}')

# 额外做一个“是否真的有数据”的快速检查(避免空面板/全 NaN 继续往下跑)
if hp.shape[1] < 50:
    raise ValueError(
        'Not enough data points loaded (too few hdates). '
        'Please check your local datasource and date range.'
    )

Sie sollten Folgendes sehen:

  • hp.shares enthält 3 Einzelbestände + 000300.SH

  • „hp.htypes“ sollte mindestens „open/high/low/close“ (und möglicherweise auch „vol“) enthalten.


4.6. 3. 派生择时因子:MACD(把信号落成一列可复用的数据)

3.1 Was dieser Abschnitt lösen soll

Zuerst wählen wir einen Timing-Faktor aus, der häufig genug vorkommt, interpretierbar genug und auch „unmittelbar verwendbar“ ist: MACD. Das Ziel dieses Abschnitts ist einfach: Berechnen Sie den MACD und erstellen Sie daraus eine neue Spalte von „HistoryPanel“, damit spätere bedingte Filterung direkt mit der Spalte verglichen werden kann.

3.2 Mindestgrundsätze

„hp.kline.macd()“ gibt ein neues „HistoryPanel“ zurück und hängt drei Spalten an „htypes“ an:

  • macd_12_26_9

  • macd_signal_12_26_9

  • macd_hist_12_26_9

Achten Sie auf diese Benennung: Standardmäßig ist sie mit einem Suffix versehen; es ist nicht das bloße „macd_hist“. In diesem Schritt geben wir die Spaltennamen aus, damit wir beim späteren Schreiben von Bedingungen nicht raten und einen Fehler machen müssen.

Wenn viele Menschen zum ersten Mal Faktorenforschung betreiben, kann es außerdem sein, dass sie Indikatoren unbewusst an verschiedenen Stellen „neu berechnen“. Auf kurze Sicht sieht es vielleicht gut aus, aber sobald Sie anfangen, mehr Spalten hinzuzufügen und mehr Diagramme zu zeichnen, kann es leicht zu einem Durcheinander kommen, bei dem „gleichnamige Spalten überschrieben werden/mehrere Kopien synonymer Spalten“ auftreten. Ein robusterer Ansatz besteht darin, Indikatoren zuerst in Spalten zu berechnen, sie explizit in „htypes“ zu schreiben und dann die nachfolgenden Bedingungen, die Aggregation und die Visualisierung nur von diesen Spalten abhängig zu machen.

3.3 Ausführbarer Code + erwartetes Ergebnis

hp_macd = hp.kline.macd(price_htype='close', fastperiod=12, slowperiod=26, signalperiod=9)
print('new htypes (tail):', hp_macd.htypes[-6:])

Erwartet: Sie werden sehen, dass „macd_hist_12_26_9“ in „htypes“ erscheint.


4.7. 4. 因子阈值 -> bool 条件 -> where() 研究 mask

4.1 Was dieser Abschnitt lösen soll

Nun formulieren wir die Timing-Regel klar: Zum Beispiel: „Wenn das MACD-Histogramm größer als 0 ist, ist es long; wenn es kleiner oder gleich 0 ist, ist es short.“ Was dieser Schritt liefern muss, sind zwei Masken:

  • „mask_long“: Welche Gitterpunkte nehmen an der Long-Portfolio-Aggregation teil?

  • „mask_short“: Welche Rasterpunkte nehmen an der Short-Portfolio-Aggregation teil?

4.2 Mindestgrundsätze

Ab 2.2.8 unterstützt „HistoryPanel“ die direkte Durchführung von Vergleichsoperationen: „hp_macd > 0“ gibt beispielsweise „numpy.ndarray(bool)“ zurück. Und „hp.where(condition)“ normalisiert verschiedene sendefähige Bedingungen in eine „(M,L,N)“-Maske mit der gleichen Form wie „hp.values“.

Dieser Schritt ist entscheidend: Wir „löschen keine Daten“, sondern „definieren die Forschungskonvention“: Rasterpunkte, bei denen die Maske „Falsch“ ist, werden in „portfolio/cum_return“ als fehlend behandelt (sie nehmen nicht an der Aggregation teil oder führen zu einer Pfadunterbrechung). Daher sei die Maske „die Forschungsregel selbst“.

Lassen Sie uns hier „Form“ etwas klarer erklären:

  • Die Timing-Regel in Ihrem Kopf lautet „ein Wahr/Falsch pro Aktie und Tag“, daher ist die natürlichste Zustandsform „(M, L)“.

  • Aber die Werte von „HistoryPanel“ sind „(M, L, N)“ mit N Feldspalten.

  • Was „where()“ tut, ist: Erweitern/Broadcasten Ihrer Bedingung in „(M, L, N)“, sodass jede nachfolgende Berechnung, die nach Gitterpunkt filtern muss, einen einzigen, einheitlichen Einstiegspunkt hat.

Wenn Sie sich erst einmal angewöhnt haben, „alle Bedingungen zuerst durch „where()“ laufen zu lassen“, ist die Wahrscheinlichkeit geringer, dass Sie später auf Shape-Fallstricke stoßen, wenn Sie Bedingungen in „portfolio(mask=…)“ und „cum_return(mask=…)“ eingeben.

4.3 Ausführbarer Code + erwartetes Ergebnis

import numpy as np

factor_col = 'macd_hist_12_26_9'

# 取单列子面板(形状 (M,L,1)),再与标量比较得到 bool ndarray
cond_long = (hp_macd[factor_col] > 0.0)     # numpy.ndarray(bool)
cond_short = (hp_macd[factor_col] <= 0.0)  # numpy.ndarray(bool)

# 规整为 (M,L,N) 研究 mask
mask_long = hp.where(cond_long)
mask_short = hp.where(cond_short)

print('cond_long shape:', getattr(cond_long, 'shape', None))
print('mask_long shape:', mask_long.shape)
print('mask_long dtype:', mask_long.dtype)

# 只做一个直觉检查:最后一天 long 有多少格点为 True
print('true_count_last_day(long):', int(mask_long[:, -1, 0].sum()))

Erwartet:

  • mask_long.shape == hp.shape

  • mask_long.dtype == bool


4.8. 5. 用 portfolio(mask=...) 聚合组合曲线,并与 benchmark 对比

5.1 Was dieser Abschnitt lösen soll

Wir führen keinen Trading-Backtest durch, sondern nur eine forschungsorientierte „grobe Aggregation“: Fassen Sie für jeden Tag die Aktien zusammen, die die Kriterien (Long/Short) erfüllen, in einer einzigen Portfoliokurve und ziehen Sie dann „000300.SH“ als Benchmark zum Vergleich heraus.

5.2 Die minimal notwendigen Grundsätze

Ein paar wichtige Punkte von „HistoryPanel.portfolio()“ (das wir verwenden):

  • mask=: folgt den gleichen Formregeln wie where(); Gitterpunkte, die False sind, nehmen nicht an der Aggregation teil

  • benchmark= + benchmark_output='tag_along': Hängen Sie die Benchmark-Zeile an die Ausgabe an

  • Die Ausgabe ist immer noch ein „HistoryPanel“ und die Zeitleiste bleibt unverändert.

Um es noch einmal zu wiederholen: Dies ist eine forschungsorientierte Aggregation; Es beinhaltet keine Transaktionskosten, unterliegt keinen Kapitalbeschränkungen und führt keine Neuausrichtung durch.

Man kann es sich als einen „gleichgewichtigen Korb gemäß einer Forschungskonvention“ vorstellen: Jeden Tag werden die Aktien, die die Kriterien erfüllen, in einer einzigen Kurve gleichgewichtet gemittelt. Sein Zweck besteht nicht darin, echten Handel zu simulieren, sondern eine grundlegendere Frage zu beantworten:

Wenn ich dieses Kriterium zur Definition eines „Haltesatzes“ verwende, weist dieser dann im Stichprobenzeitraum systematische Leistungsunterschiede auf?

Erst wenn die Antwort auf diese Frage „Ja“ lautet, lohnt es sich, weiter zu investieren und es zu einem echten Trading-Strategie-Backtest auszubauen.

5.3 Ausführbarer Code + erwartete Ergebnisse

benchmark = '000300.SH'

pf_long = hp.portfolio(
    htypes='close',
    mode='equal',
    mask=mask_long,
    benchmark=benchmark,
    benchmark_output='tag_along',
    new_share_name='LONG',
)

pf_short = hp.portfolio(
    htypes='close',
    mode='equal',
    mask=mask_short,
    benchmark=benchmark,
    benchmark_output='tag_along',
    new_share_name='SHORT',
)

print('pf_long.shares:', pf_long.shares)
print('pf_long.htypes:', pf_long.htypes)
print('pf_long.shape:', pf_long.shape)

Erwartet: „pf_long.shares“ enthält zwei Zeilen: „LONG“ und „000300.SH“.


4.9. 6. normalize / cum_return + CAGR:把曲线变成可比较的年化摘要

6.1 Was dieser Abschnitt lösen soll

Wenn man zwei Kurven nebeneinander sieht, kann man den Trend mit bloßem Auge erkennen, aber es ist schwierig, schnell zusammenzufassen: „Wie viel stärker ist in diesem Forschungszeitraum genau gegenüber der Benchmark?“ Also machen wir zwei Dinge:

  1. „normalisieren“: Startpunkt ausrichten (zum einfacheren visuellen Vergleich)

  2. „cum_return + CAGR“: Erstellt eine wiederverwendbare jährliche Übersichtstabelle

6.2 Das Prinzip der Mindestnotwendigkeit

  • „normalize(base_index=0)“ skaliert den Basispunkt auf 1,0 (Forschungskonvention), was ideal für die Darstellung und den Vergleich im selben Diagramm ist

  • cum_return(method='simple') gibt kumulative Rückgaben cumret_* aus

  • Das Wesentliche der CAGR ist eine „äquivalente jährliche Wachstumsrate“:

    [ \text{CAGR}=(1+R)^{1/T}-1 ]

Dabei ist (R) die kumulative Rendite über das Intervall (Endwert) und (T) die Anzahl der Jahre.

Fügen wir hier noch einen Satz zum Thema „Warum die CAGR berechnen?“ hinzu: Sehr oft vergleichen wir Zeiträume unterschiedlicher Länge (ein halbes Jahr, ein Jahr, zwei Jahre). Wenn man nur die kumulative Rendite betrachtet, kann man leicht zu dem Schluss kommen: „Je länger der Zeitraum, desto beeindruckender sieht er aus.“ CAGR wandelt es in „äquivalentes jährliches Wachstum“ um, wodurch Ergebnisse aus verschiedenen Forschungshorizonten einfacher miteinander verglichen werden können.

6.3 Ausführbarer Code + erwartete Ergebnisse

import pandas as pd

def _years_between(hdates) -> float:
    idx = pd.DatetimeIndex(hdates)
    days = (idx[-1] - idx[0]).days
    return max(1e-9, days / 365.25)

def _cagr_from_cumret(cumret_end: float, years: float) -> float:
    return (1.0 + cumret_end) ** (1.0 / years) - 1.0

years = _years_between(pf_long.hdates)

cr_long = pf_long.cum_return(htypes='close', method='simple')   # 输出列 cumret_close
cr_short = pf_short.cum_return(htypes='close', method='simple')

cumret_long_end = float(cr_long.values[cr_long.shares.index('LONG'), -1, 0])
cumret_short_end = float(cr_short.values[cr_short.shares.index('SHORT'), -1, 0])

# benchmark 行同样在 shares 里(tag_along)
cumret_bm_long_end = float(cr_long.values[cr_long.shares.index('000300.SH'), -1, 0])
cumret_bm_short_end = float(cr_short.values[cr_short.shares.index('000300.SH'), -1, 0])

summary = pd.DataFrame(
    {
        'cum_return_end': [cumret_long_end, cumret_short_end, cumret_bm_long_end],
        'CAGR': [
            _cagr_from_cumret(cumret_long_end, years),
            _cagr_from_cumret(cumret_short_end, years),
            _cagr_from_cumret(cumret_bm_long_end, years),
        ],
    },
    index=['LONG', 'SHORT', '000300.SH'],
)

print(summary)

Sie sollten eine dreizeilige Übersichtstabelle sehen können. Es wird empfohlen, auf mindestens zwei Dinge zu achten:

  • LONG vs. 000300.SH: Hat es wirklich eine Outperformance erzielt (höhere CAGR und/oder höhere kumulative Rendite)?

  • KURZE Leistung: Es muss nicht unbedingt „Geld verlieren“, aber es hilft uns zu beurteilen, ob die Bedingung wirklich die Stichprobe trennt (d. h. ob der Unterschied zwischen dem LANGEN und dem KURZEN Ende offensichtlich ist).


4.10. 7. 可视化解释:用 plot(highlight=...) 把“触发点”标回图上

7.1 Was dieser Abschnitt lösen soll

Wir haben bereits die Rückkehrkurve, aber uns fehlt noch der letzte Teil: Erklärung. In diesem Schritt machen wir etwas „Überprüfungsfreundliches“: Markieren Sie auf dem Candlestick-Chart einer einzelnen Aktie die Triggerpunkte, damit die Leser auf einen Blick sehen können, „wann die Timing-Bedingung ausgelöst wurde“.

7.2 Das Prinzip der Mindestnotwendigkeit

„HistoryPanel.plot(highlight=…)“ unterstützt zwei häufige Verwendungen:

  • 简写:highlight='max'/'min'

  • 显式:highlight={'condition': <1D bool over time>, 'style': {...}}

Eines ist zu beachten: Im statischen Rendering-Pfad orientiert sich „Bedingung“ eher an einer 1D-Zeitachse (wird für Streumarkierungen im Diagramm verwendet). Daher ist es hier robuster, zum Hervorheben einen 1D-Bool auf einer Einzelstock-Zeitachse zu verwenden.

7.3 Ausführbarer Code + erwartete Ausgabe

import numpy as np

primary_share = '000001.SZ'

# 从 cond_long(M,L,1 或 M,L,N 的 bool)里抽出该 share 的时间轴 1D 条件
si = hp_macd.shares.index(primary_share)
cond_1d = np.asarray(cond_long[si, :, 0], dtype=bool).ravel()

# 为了让图更清爽,这里只看最后 200 个交易日(你也可以改成全区间)
hp_one = hp.loc[-200:]

fig = hp_one.plot(
    shares=[primary_share],
    interactive=True,
    highlight={'condition': cond_1d[-200:], 'style': {'marker': 'x', 's': 50}},
)
fig

Erwartetes Ergebnis: Sie sehen eine Reihe hervorgehobener Punkte im Diagramm (entsprechend den Zeiten, in denen „macd_hist_12_26_9 > 0“ ist). Der Wert dieses Schrittes liegt darin, dass Sie sehen, dass sich ein Segment der Portfoliokurve plötzlich verschlechtert, und so zum Einzelaktiendiagramm zurückkehren können, um schnell zu überprüfen, ob die Triggerpunkte in einer unruhigen Spanne gruppiert sind, ob Sie hin und her „gepeitscht“ werden, und dann entscheiden können, ob Sie als Nächstes Filterbedingungen hinzufügen möchten (z. B. Trendfilter, Volatilitätsfilter usw.).


4.11. 8. 完整代码(单函数可跑版本)

Nachfolgend finden Sie eine vollständige „ausführbare Einzelfunktionsversion“, die Sie in ein Notebook kopieren und mit einem Klick ausführen können. Es bewirkt drei Dinge:

  • Führen Sie die vollständige Forschungsschleife von „MACD -> Maske -> Portfolio -> CAGR“ durch.

  • Zeichnen Sie die normalisierten Kurven von LONG/SHORT/benchmark;

  • Zeichnen Sie Einzelaktiendiagramme und markieren Sie Triggerpunkte (zur einfacheren Erklärung und Überprüfung).

import numpy as np
import pandas as pd
import qteasy as qt


def demo_vertical_timing(
        stocks: list,
        benchmark: str = '000300.SH',
        start: str = '20220101',
        end: str = '20221231',
        primary_share: str = '000001.SZ',
):
    \"\"\"演示纵向择时研究闭环:MACD 阈值 -> mask -> 组合曲线 -> CAGR -> 高亮解释。

    Parameters
    ----------
    stocks : list
        个股代码列表(建议 3~15 只,太多不利于解释)。
    benchmark : str, default '000300.SH'
        基准指数代码(示例使用沪深 300)。
    start : str, default '20220101'
        起始日期(YYYYMMDD)。
    end : str, default '20221231'
        结束日期(YYYYMMDD)。
    primary_share : str, default '000001.SZ'
        用于做“触发点高亮解释”的单只股票代码。

    Returns
    -------
    dict
        结果对象集合,便于你在 Notebook 里继续查看:
        - hp: 原始面板
        - hp_macd: 含 MACD 列的面板
        - pf_long/pf_short: 组合面板(含 benchmark 行)
        - summary: CAGR 摘要表(DataFrame)
        - fig_pf: 组合对比图
        - fig_one: 单股高亮图
    \"\"\"
    shares = list(stocks) + [benchmark]

    hp = qt.get_kline(
        shares=shares,
        start=start,
        end=end,
        freq='D',
        as_panel=True,
    )

    required = {'open', 'high', 'low', 'close'}
    missing = [c for c in required if c not in set(hp.htypes)]
    if missing:
        raise ValueError(f'Missing required OHLC columns in htypes: {missing}')
    if benchmark not in hp.shares:
        raise ValueError(f'Benchmark {benchmark} not found in shares: {hp.shares}')
    if hp.shape[1] < 50:
        raise ValueError(
            'Not enough data points loaded (too few hdates). '
            'Please check your local datasource and date range.'
        )
    if primary_share not in hp.shares:
        raise ValueError(f'primary_share "{primary_share}" not found in shares')

    # 1) 派生因子:MACD(默认 12_26_9)
    hp_macd = hp.kline.macd(price_htype='close', fastperiod=12, slowperiod=26, signalperiod=9)
    factor_col = 'macd_hist_12_26_9'
    if factor_col not in hp_macd.htypes:
        raise ValueError(f'Required factor htype "{factor_col}" not found after macd()')

    # 2) 条件 -> mask(研究口径)
    cond_long = (hp_macd[factor_col] > 0.0)
    cond_short = (hp_macd[factor_col] <= 0.0)
    mask_long = hp.where(cond_long)
    mask_short = hp.where(cond_short)

    # 3) 组合聚合 + benchmark
    pf_long = hp.portfolio(
        htypes='close',
        mode='equal',
        mask=mask_long,
        benchmark=benchmark,
        benchmark_output='tag_along',
        new_share_name='LONG',
    )
    pf_short = hp.portfolio(
        htypes='close',
        mode='equal',
        mask=mask_short,
        benchmark=benchmark,
        benchmark_output='tag_along',
        new_share_name='SHORT',
    )

    # 4) cum_return -> CAGR 摘要
    def _years_between(hdates) -> float:
        idx = pd.DatetimeIndex(hdates)
        days = (idx[-1] - idx[0]).days
        return max(1e-9, days / 365.25)

    def _cagr_from_cumret(cumret_end: float, years: float) -> float:
        return (1.0 + cumret_end) ** (1.0 / years) - 1.0

    years = _years_between(pf_long.hdates)
    cr_long = pf_long.cum_return(htypes='close', method='simple')
    cr_short = pf_short.cum_return(htypes='close', method='simple')

    cumret_long_end = float(cr_long.values[cr_long.shares.index('LONG'), -1, 0])
    cumret_short_end = float(cr_short.values[cr_short.shares.index('SHORT'), -1, 0])
    cumret_bm_end = float(cr_long.values[cr_long.shares.index(benchmark), -1, 0])

    summary = pd.DataFrame(
        {
            'cum_return_end': [cumret_long_end, cumret_short_end, cumret_bm_end],
            'CAGR': [
                _cagr_from_cumret(cumret_long_end, years),
                _cagr_from_cumret(cumret_short_end, years),
                _cagr_from_cumret(cumret_bm_end, years),
            ],
        },
        index=['LONG', 'SHORT', benchmark],
    )
    print('\\n[CAGR summary]')
    print(summary)

    # 5) 图:组合曲线对比(先 normalize,便于肉眼比较)
    pf_view = pf_long.normalize(htypes='close', base_index=0)
    fig_pf = pf_view.plot(interactive=True)

    # 6) 图:单股触发点解释(1D 时间轴条件)
    si = hp_macd.shares.index(primary_share)
    cond_1d = np.asarray(cond_long[si, :, 0], dtype=bool).ravel()
    lookback = min(200, len(cond_1d))
    fig_one = hp.loc[-lookback:].plot(
        shares=[primary_share],
        interactive=True,
        highlight={'condition': cond_1d[-lookback:], 'style': {'marker': 'x', 's': 50}},
    )

    return {
        'hp': hp,
        'hp_macd': hp_macd,
        'pf_long': pf_long,
        'pf_short': pf_short,
        'summary': summary,
        'fig_pf': fig_pf,
        'fig_one': fig_one,
    }


res = demo_vertical_timing(
    stocks=['000001.SZ', '600519.SH', '300750.SZ'],
    benchmark='000300.SH',
    start='20220101',
    end='20221231',
    primary_share='000001.SZ',
)
res['fig_pf']

Hinweis: Das obige „Normalisieren“ dient nur dazu, den visuellen Vergleich intuitiver zu gestalten. Für Statistiken sollten Sie sich weiterhin auf „cum_return“ oder die Rohpreisreihe verlassen.


4.12. 9. 小结与边界

Zu diesem Zeitpunkt haben wir bereits eine vollständige Forschungsschleife für „Timing bei einem kleinen Satz von Instrumenten“ durchlaufen: Daten -> Faktor -> Bool-Bedingung -> Where-Maske -> Portfolio + Benchmark -> cum_return + CAGR -> plot(highlight)-Erklärung.

Hinweis: Bei „portfolio/cum_return“ handelt es sich hier ausschließlich um forschungsorientierte Berechnungen und enthalten keine vollständige Backtesting-Semantik wie Transaktionskosten, Slippage, Abwicklung, Kapitalbeschränkungen usw. Wenn Sie die Forschungslogik in einen echten Strategie-Backtest migrieren möchten, wird empfohlen, den Faktor/die Bedingung als für die Strategie verwendbare Datenspalten oder Signale auszugeben und sie an „Operator/Backtester“ zu übergeben, um die Semantik der Handelsebene zu verwalten.


4.13. Anhang: Abbildungsindex (empfohlen zur Erstellung/Screenshot im Notebook)

Abbildung

Empfohlene Platzierung

Was Sie sehen werden

img/3.1_minimal_run.png

§0 Minimaler lauffähiger Kickoff

Beweisen Sie, dass die get_kline -> plot-Pipeline funktioniert

img/3.1_pf_compare.png

§0.5 oder §6

Normalisierter Kurvenvergleich von LONG/SHORT/000300.SH

img/3.1_highlight_one_share.png

§7

Markieren Sie Triggerpunkte in einem Einzelaktiendiagramm (zur einfacheren Überprüfung und Erklärung).