5. Verwendung von HistoryPanel zur Untersuchung von Querschnittsfaktoren bei der Aktienauswahl
Aktienauswahl im Querschnitt: Multi-Faktor-Gewichtung -> gruppierte Portfoliorenditen und CAGR vergleichen
Bei der Aktienauswahl im Querschnitt geraten wir oft in eine Situation, die sich „sehr real, aber sehr unangenehm“ anfühlt:
Wir verfügen eindeutig über eine Reihe von Aktien (Dutzende, Hunderte) und wissen auch, dass wir mehrere Kennzahlen zum Screening verwenden sollten (KGV, PB, EBITDA, Momentum, Volatilität …).
Sobald Sie die Bedingungen jedoch in den Code geschrieben haben, kann es leicht passieren, dass die Meldungen „Formen stimmen nicht überein“, „Übertragung der Bedingungen sind falsch“ oder „Die überprüften Bestände sind instabil“ lauten.
Am Ende erhalten Sie eine einzelne Portfoliokurve, können jedoch nicht klar erklären, welche Bedingungen tatsächlich für die Renditeunterschiede verantwortlich sind
Dieses Tutorial folgt einem „Erst zum Laufen bringen, dann verbessern“-Rhythmus, um diese Pipeline in einen wiederverwendbaren Forschungsworkflow zu integrieren: Multifaktor (Querschnitt) -> Querschnittsbedingungen -> „Wo“-Maske -> „Portfolio + Benchmark“ -> „Normalisieren/Cum_Return + CAGR“ -> „Plot/Hervorheben“-Erklärung.
Um die Positionierung noch einmal klarzustellen: Dies ist immer noch eine forschungsorientierte grobe Aggregation, keine Trading-Backtest-Engine.
5.1. 0. 开场:先跑通一个“横向筛选 -> 组合曲线对比”的最小版本
Wir werden vorerst weder viele Faktoren noch fein abgestimmte Parameter verfolgen. Ein minimal ausführbarer Beweis muss nur zwei Dinge tun:
Mit mehreren Freigaben können Sie ein Querschnittsscreening durchführen;
Am Ende können Sie eine kombinierte Kurve zeichnen und diese mit „000300.SH“ vergleichen.
import qteasy as qt
benchmark = '000300.SH'
shares = [
'000001.SZ', '600519.SH', '300750.SZ', '000333.SZ', '600036.SH',
'601318.SH', '002415.SZ', '000858.SZ', '600276.SH', '000725.SZ',
benchmark,
]
hp = qt.get_kline(
shares=shares,
start='20220101',
end='20221231',
freq='D',
as_panel=True,
)
fig = hp.plot(interactive=True)
fig
Allerdings reicht es bei weitem nicht aus, nur „in der Lage zu sein, es zu planen“. Wenn wir es wirklich als Recherchetool für die tägliche Aktienauswahl nutzen wollen, werden wir auf mindestens die folgenden Probleme stoßen:
So passen Sie Multifaktor-Bedingungen an: Konzepte wie PE/PB/Momentum/Volatilität sind leicht zu verstehen, aber sobald Sie Code schreiben, werden Sie auf Fallstricke stoßen: Einige Faktoren sind „(M,L)“, andere haben Sie als „(M,L,1)“ geschrieben, und darüber hinaus verfügt „HistoryPanel“ über eine eigene dritte Dimension für Feldspalten – so ist es für Rundfunk leicht, „übereinzustimmen, aber nicht so, wie Sie denken“. Das Schlimmste daran ist, dass diese Fehler oft keine Fehler auslösen; Sie lassen die Ergebnisse einfach „falsch“ aussehen.
Die Aktienauswahl ist eine Querschnittsentscheidung: Bei der Querschnittsauswahl geht es nicht darum, „jede Aktie einzeln zu beurteilen“, sondern „am selben Tag eine Teilmenge aus einer Reihe von Aktien auszuwählen“. Das bedeutet, dass sich Ihr Bestand täglich ändern kann: 10 Aktien heute, 3 morgen und möglicherweise 0 übermorgen. Wenn Sie „jeden Tag den ausgewählten Korb“ nicht explizit verfestigen (z. B. mit einer Maske), können Sie einfach nicht überprüfen, „welche an diesem Tag tatsächlich ausgewählt wurden“.
Ohne einen Benchmark-Vergleich gibt es keine Schlussfolgerung: Eine kombinierte Kurve, die gut aussieht, bedeutet nicht, dass sie effektiv ist – es könnte sich lediglich um eine Markt-Beta handeln. Geben Sie „000300.SH“ als Referenz ein; Es beantwortet zumindest die wichtigste Frage: Schafft dieser Bildschirm Alpha oder folgt er nur dem breiten Markt?
Retouren ohne Erklärung: Beim Querschnittsscreening kann es leicht dazu kommen, dass „nur noch eine Kurve übrig ist“. Aber echte Forschung bedarf einer Erklärung: Kam es an den Tagen mit den größten Renditeunterschieden zufällig zu einer Stilrotation? Haben uns die Screening-Kriterien in eine Situation mit hoher Volatilität/hohem Drawdown gedrängt? Können wir schnell das „wichtigste Divergenzsegment“ identifizieren und dann entscheiden, welcher Faktor als nächstes gestärkt werden soll?
Glücklicherweise können diese Fähigkeiten alle Schritt für Schritt ausgefüllt werden. Als nächstes beginnen wir mit der „Faktorkonstruktion“.
5.2. 0.5 Zeigen Sie zunächst das Endergebnis (was am Ende herauskommt)
Nachdem Sie diesem Artikel gefolgt sind, erhalten Sie drei sehr „forschungsfreundliche“ Ergebnisse:
Ein Vergleichsdiagramm der Portfoliokurve: „LONG / SHORT / 000300.SH“ (oder zumindest „LONG / 000300.SH“) normalisiert, um bei 1,0 zu beginnen, sodass Sie auf einen Blick erkennen können, ob die Screening-Regel innerhalb des Stichprobenzeitraums Unterscheidungskraft hat.
Eine CAGR-Übersichtstabelle: Konvertieren Sie kumulierte Renditen in eine auf das Jahr umgerechnete Kennzahl, um den Vergleich über verschiedene Zeiträume hinweg zu erleichtern.
Ein einzelnes Hervorhebungsdiagramm für „wichtige Divergenztage/Divergenzsegmente“: Markieren Sie die Punkte, an denen das Long-Portfolio am deutlichsten von der Benchmark abweicht, um die Überprüfung und Erklärung zu erleichtern.
5.3. 1. 目标(我们这篇文章要完成什么)
Holen Sie sich den
HistoryPanelfür mehrere Freigaben +000300.SHKonstruieren Sie Multifaktoren (zwei Routen parallel: eine Proxy-Faktor-Version mit festem Schwellenwert + eine optionale True-Value-Version)
Kombinieren Sie Multi-Faktor-Bedingungen in einem „(M,L)“-Bool und verwenden Sie „where()“, um die Forschungsmaske zu generieren
Verwenden Sie „portfolio(mask=…)“, um die Long/Short-Portfoliokurven zu erhalten und sie mit der Benchmark zu vergleichen
Verwenden Sie
normalize/cum_return, um die CAGR-Tabelle abzuleitenVerwenden Sie „plot(highlight=…)“, um das „Tages-/Schlüsseldivergenzsegment mit maximaler Differenz“ hervorzuheben und es erklärbar zu machen
Am Ende stellen wir einen vollständigen Code bereit, der „als einzelne Funktion ausgeführt wird“.
5.5. 3. 多因子构造(两条路线并列):固定阈值代理版 + 可选真实估值版
3.1 Was dieser Abschnitt lösen soll
Was wir in diesem Schritt wollen, ist: eine Faktormatrix, die direkt in Screening-Bedingungen kombiniert werden kann. In einem „Querschnitt“ möchten wir, dass jeder Faktor letztendlich in derselben Form landet: „(M, L)“.
Der Einfachheit halber teilen wir uns hier in zwei Tracks auf:
Route A (empfohlen für Anfänger): Leiten Sie „Proxy-Faktoren“ vollständig aus Marktdaten/technischen Indikatoren ab, gepaart mit festen Schwellenwerten, um sicherzustellen, dass jeder sie durchlaufen kann
Track B (optionale Erweiterung): Wenn Ihre lokale Datenquelle bereits Bewertungsfelder wie PE/PB/EBITDA enthält, ersetzen Sie diese hier
Beide Routen geben letztendlich das gleiche „cond_long/cond_short“ aus und der nachfolgende Workflow ist genau der gleiche.
3.2 Mindestgrundsätze
„HistoryPanel.kline.*“ gibt ein neues Panel mit zusätzlichen Spalten zurück, zum Beispiel:
sma_20macd_hist_12_26_9bbands_upper_20_2_2等
Alle diese Spalten können über „hp.htypes.index(name)“ in einer 2D-Matrix von „(M, L)“ lokalisiert werden. Dann können wir Multi-Faktor-Bedingungen mit festen Schwellenwerten schreiben und sicherstellen, dass die Endbedingung ein „(M, L)“-Bool ist.
3.3 Track A: Proxy-Faktoren (feste Schwellenwerte; empfohlen, dies zuerst zum Laufen zu bringen)
3.3.1 Was zu lösen ist
Wir verwenden drei „sehr intuitive“ Proxy-Faktoren, um den Lesern den schnellen Einstieg zu erleichtern:
Wert-Proxy: „close / sma20“ (je geringer die Abweichung vom gleitenden Durchschnitt, desto „günstiger“ ist es)
Momentum-Proxy:
macd_hist(Stärke/Schwäche)Risiko-Proxy: Bollinger-Bandbreite (Volatilitätsgröße)
Zwei Punkte der „Ehrlichkeit in Forschungskonventionen“ sind hier hervorzuheben:
Dabei handelt es sich allesamt um Proxies, nicht um eine „echte Bewertung“ im finanziellen Sinne. Ihr Wert liegt darin: Jeder kann sie aus Marktdaten ableiten, und unter bestimmten Stilen können sie tatsächlich einen Schichtungseffekt erzeugen.
Sie haben auch klare Einschränkungen: „close/sma20“ kann einen Trend mit billig/teuer verwechseln; Der MACD könnte weiterhin in einem unruhigen Bereich bleiben; Ein Bandbreitenfilter filtert auch „durch Volatilität geschaffene Chancen“ heraus. Der Schwerpunkt dieses Artikels liegt darauf, die Pipeline durchgängig laufen zu lassen. Die Schwellenwerte selbst sind lediglich ein reproduzierbarer Ausgangspunkt.
3.3.2 Ausführbarer Code + erwartete Ergebnisse
import numpy as np
hp2 = hp.kline.sma(window=20, price_htype='close') # sma_20
hp2 = hp2.kline.macd(price_htype='close') # macd_hist_12_26_9
hp2 = hp2.kline.bbands(window=20, price_htype='close') # bbands_*_20_2_2
vals = hp2.values.astype(float)
close = vals[:, :, hp2.htypes.index('close')]
sma20 = vals[:, :, hp2.htypes.index('sma_20')]
macd_hist = vals[:, :, hp2.htypes.index('macd_hist_12_26_9')]
upper = vals[:, :, hp2.htypes.index('bbands_upper_20_2_2')]
mid = vals[:, :, hp2.htypes.index('bbands_middle_20_2_2')]
lower = vals[:, :, hp2.htypes.index('bbands_lower_20_2_2')]
value_proxy = close / sma20
momentum_proxy = macd_hist
risk_proxy = (upper - lower) / mid
# 固定阈值(入门优先:简单、可跑、可理解)
A_VALUE = 1.02 # 强一点:价格明显强于均线才算“强势”
C_MOM = 0.0 # MACD 柱 > 0 视为偏多
B_RISK = 0.18 # 带宽太大视为波动过强,先过滤掉
cond_long = (value_proxy > A_VALUE) & (momentum_proxy > C_MOM) & (risk_proxy < B_RISK)
cond_short = (value_proxy < 1.0 / A_VALUE) & (momentum_proxy < -C_MOM) & (risk_proxy < B_RISK)
print('cond_long shape:', cond_long.shape) # 期望 (M, L)
print('cond_short shape:', cond_short.shape)
print('selected_count_last_day(long):', int(cond_long[:, -1].sum()))
An diesem Punkt haben wir die „Kernzutaten“ für die Querschnittsfilterung erhalten: „cond_long/cond_short (M,L bool)“.
Als nächstes müssen wir noch eine ganz praktische Plausibilitätsprüfung durchführen: wie viele Aktien jeden Tag ausgewählt werden. Wenn diese Zahl oft 0 ist (ein leerer Korb), wird Ihre Portfoliokurve unregelmäßig sein und Ihre Schlussfolgerungen werden instabil sein; Wenn es sich bei dieser Zahl fast immer um alle Aktien handelt, ist das Screening bedeutungslos.
Sie können diese Prüfung hinzufügen (sie ändert nichts an der Logik; sie hilft uns nur bei der Beurteilung, ob die Schwellenwerte „zu streng/zu locker“ sind):
selected_count_by_day = cond_long.sum(axis=0) # (L,)
print('selected_count stats (long):')
print(' min/max:', int(selected_count_by_day.min()), int(selected_count_by_day.max()))
print(' mean:', float(selected_count_by_day.mean()))
print(' p10/p50/p90:', np.quantile(selected_count_by_day.astype(float), [0.1, 0.5, 0.9]))
3.4 Route B: echte Bewertungsfaktoren (optionale Erweiterung; Feldnamen unterliegen „hp.htypes“)
3.4.1 Was muss gelöst werden
Wenn Ihre lokale Datenquelle bereits Bewertungsfelder heruntergeladen hat (z. B. PE/PB/EBITDA), können wir diese austauschen. In diesem Abschnitt werden wir Feldnamen nicht fest codieren, da die htype-Benennung je nach Datenquelle/Datentyp unterschiedlich sein kann. Der robusteste Ansatz ist:
Erster Druck
hp.htypesFinden Sie die tatsächlichen Spaltennamen in Ihrem lokalen Setup
Schreiben Sie dann die Bedingungen unter Verwendung fester Schwellenwerte
3.4.2 Ausführbarer Code (veranschaulichend)
print('available htypes:', hp.htypes)
# 假设你在 htypes 里找到了这三个字段(名称以你的本地为准)
# pe_name = 'pe' or 'pe_ttm' ...
# pb_name = 'pb' ...
# ebitda_name = 'ebitda' ...
# pe = hp.values[:, :, hp.htypes.index(pe_name)]
# pb = hp.values[:, :, hp.htypes.index(pb_name)]
# ebitda = hp.values[:, :, hp.htypes.index(ebitda_name)]
# 固定阈值示例(仅示意,阈值需要你按资产池与口径调整)
# cond_long = (pe < 15.0) & (pb < 2.0) & (ebitda > 1e9)
Solange Sie immer noch „(M, L)“ „cond_long/cond_short“ erhalten, ist der Rest des Workflows genau der gleiche wie bei Route A.
Wenn Sie feststellen, dass Ihre lokalen Daten einfach nicht über diese Felder verfügen, ist das in Ordnung: Genau aus diesem Grund haben wir die „Proxy-Faktor-Version“ in den Haupterzählpfad aufgenommen. Route B existiert als erweiterter Zweig, Sie können jedoch weiterhin die gesamte Forschungsschleife durchgängig ausführen, ohne sich darauf verlassen zu müssen.
5.6. 4. 横向筛选:条件组合 -> where() 研究 mask
4.1 Was dieser Abschnitt lösen soll
Wir verwandeln „cond_long/cond_short“ in eine Research-Maske, die direkt in „portfolio(mask=…)“ eingespeist werden kann.
4.2 Mindestgrundsätze
„hp.where()“ unterstützt die Umformung boolescher „(M, L)“-Bedingungen in eine „(M, L, N)“-Maske. Dieser Schritt verfestigt die „Querschnitts-Screening-Regeln“ zu einer „Forschungsdefinition“, und alle nachfolgenden Aggregations- und Renditeberechnungen folgen dieser als Standard.
Dies ist auch eine der wertvollsten Gewohnheiten, die man sich bei der Querschnittsforschung aneignen sollte: Behalten Sie niemals nur eine einzige „Portfoliokurve“ bei – behalten Sie ausdrücklich auch die Regel bei, „welche Namen jeden Tag in den Korb kommen“. Die Maske ist diese Regel. Damit können Sie Bewertungsfragen beantworten wie „Wen genau haben wir an diesem Tag ausgewählt?“
4.3 Ausführbarer Code + erwartetes Ergebnis
mask_long = hp2.where(cond_long)
mask_short = hp2.where(cond_short)
print('mask_long shape:', mask_long.shape) # 期望 (M,L,N)
print('mask_long dtype:', mask_long.dtype)
5.7. 5. 两组组合曲线 + benchmark:portfolio(mask=...)
5.1 Was dieser Abschnitt lösen soll
Fassen Sie für jeden Handelstag die Aktien, die die Bedingungen erfüllen, in einer Portfoliokurve (Long/Short) zusammen und vergleichen Sie diese mit „000300.SH“.
5.2 Die minimal notwendigen Grundsätze
„Portfolio“ dient der groben Research-Aggregation, nicht der Handelsausführung
„benchmark_output=‘tag_along“ fügt die Benchmark-Zeile an die Ausgabe an und erleichtert so den Vergleich im selben Diagramm
Sie können es sich so vorstellen: Nehmen Sie für jeden Tag einen gleichgewichteten Durchschnitt der „An diesem Tag ausgewählten Aktien“, um eine Kurve zu erhalten. Da sich die Menge jeden Tag ändert, beantwortet diese Kurve im Wesentlichen eine Frage:
Wenn ich jeden Tag nur „die Charge halte, die den Kriterien entspricht“, wie verhält sich dieser dynamische Korb dann auf lange Sicht?
5.3 Ausführbarer Code + erwartete Ergebnisse
benchmark = '000300.SH'
pf_long = hp2.portfolio(
htypes='close',
mode='equal',
mask=mask_long,
benchmark=benchmark,
benchmark_output='tag_along',
new_share_name='LONG',
)
pf_short = hp2.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.shape:', pf_long.shape)
5.8. 6. normalize / cum_return + CAGR:给出可比较的年化摘要表
6.1 Was dieser Abschnitt lösen soll
Wir wollen sowohl „die Kurve betrachten“ als auch „eine wiederverwendbare numerische Zusammenfassung haben“. Also machen wir zwei Dinge:
„normalisieren“: Startpunkt ausrichten (zum einfacheren visuellen Vergleich)
cum_return + CAGR: Ausgabe einer Übersichtstabelle
6.2 Das Prinzip der Mindestnotwendigkeit
„cum_return“ gibt die kumulative Rückgabe „cumret_*“ aus; Wenn Sie den Endwert zusammen mit der Anzahl der Jahre verwenden, können Sie die CAGR ableiten. (Die genaue Definition von CAGR finden Sie in der vorherigen Diskussion; wir werden die Ableitung hier nicht erweitern.)
6.3 Ausführbarer Code (veranschaulichend)
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_long_end = float(cr_long.values[cr_long.shares.index('LONG'), -1, 0])
cumret_bm_end = float(cr_long.values[cr_long.shares.index('000300.SH'), -1, 0])
print('CAGR(long):', _cagr_from_cumret(cumret_long_end, years))
print('CAGR(bm):', _cagr_from_cumret(cumret_bm_end, years))
Ich schlage vor, dass Sie es in einer kleinen Tabelle organisieren (mindestens mit drei Zeilen: LONG/SHORT/Benchmark), damit die Leser auf einen Blick vergleichen können:
cr_short = pf_short.cum_return(htypes='close', method='simple')
cumret_short_end = float(cr_short.values[cr_short.shares.index('SHORT'), -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', '000300.SH'],
)
print('\\n[CAGR summary]')
print(summary)
5.9. 7. 可视化与解释:用 plot(highlight=...) 高亮“关键差异日”
7.1 Was dieser Abschnitt lösen soll
Wenn man die Querschnittsforschung bis zum Ende durchführt, ist die Art der Frage, die am meisten einer Erklärung bedarf, folgende: Was genau ist in dem Abschnitt passiert, in dem Long und Benchmark am stärksten voneinander abweichen?
Deshalb verwenden wir „Hervorheben“, um ein „Tag der wichtigsten Divergenz“-Segment hervorzuheben (z. B. den Punkt der maximalen/minimalen kumulativen Rendite oder ein von Ihnen selbst gewähltes Intervall) und die Aufmerksamkeit des Lesers wieder auf das Diagramm zu lenken.
7.2 Das Prinzip der Mindestnotwendigkeit
Highlight unterstützt die Abkürzung „max“/„min“ sowie eine 1D-Bool-Bedingung. Um das Tutorial stabil zu halten, demonstrieren wir zunächst die Kurzschriftversion (wobei die Wahrscheinlichkeit gering ist, dass Sie stolpern) und stellen später die 1D-Bool-Form zur Verfügung.
7.3 Ausführbarer Code + erwartete Ausgabe
fig = pf_long.plot(interactive=True, highlight='max')
fig
Erwartetes Ergebnis: Das Diagramm markiert den Maximalpunkt der LONG-Portfoliokurve (oder den Maximalpunkt, wie durch ein bestimmtes Diagramm definiert). In der Querschnittsforschung betrachten wir dies normalerweise als „Erinnerung“: Um den Maximalpunkt herum liegt oft der Zeitraum, in dem es lange Zeit den stärksten Rückenwind im Verhältnis zum Markt gab, und es lohnt sich, zurückzublicken, um zu sehen, ob uns die Screening-Kriterien zu einem bestimmten Stil geführt haben (z. B. starker Trend, geringe Volatilität usw.).
Wenn Sie sich stärker an der „Divergenz zwischen Long und Benchmark“ orientieren möchten, können Sie einen praktischeren Ansatz verwenden: Berechnen Sie zunächst die Überschuss-Rendite-Kurve (die Differenz zwischen Long und Benchmark), extrahieren Sie dann die Position des Maximalpunkts auf dieser Überschusskurve und wandeln Sie ihn in eine 1D-Bool-Hervorhebungsbedingung um. (Dies ist eine optionale Erweiterung; Sie müssen in der Haupterzählung nicht näher darauf eingehen.)
5.10. 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 umfasst:
Spur A (Proxy-Faktor mit festem Schwellenwert) als Hauptlinie;
Jeden Tag eine Plausibilitätsprüfung der ausgewählten Nummer;
Eine vollständige geschlossene Schleife von „wo -> Portfolio -> cum_return -> CAGR“;
Eine stabile Demo von
plot(highlight=...).
import numpy as np
import pandas as pd
import qteasy as qt
def demo_horizontal_multifactor(
shares: list,
benchmark: str = '000300.SH',
start: str = '20220101',
end: str = '20221231',
):
\"\"\"演示横向多因子截面筛选:代理因子 -> 每日篮子 -> portfolio -> CAGR -> 高亮解释。
Parameters
----------
shares : list
股票池(必须包含 benchmark;建议 30~80 只更像“横向筛选”)。
benchmark : str, default '000300.SH'
基准指数代码。
start : str, default '20220101'
起始日期(YYYYMMDD)。
end : str, default '20221231'
结束日期(YYYYMMDD)。
Returns
-------
dict
包含 hp/hp2/pf_long/pf_short/summary/fig 等结果对象。
\"\"\"
if benchmark not in shares:
raise ValueError('benchmark must be included in shares')
hp = qt.get_kline(
shares=shares,
start=start,
end=end,
freq='D',
as_panel=True,
)
if 'close' not in hp.htypes:
raise ValueError('Missing close column in htypes')
if hp.shape[1] < 50:
raise ValueError(
'Not enough data points loaded (too few hdates). '
'Please check your local datasource and date range.'
)
# 1) 路线 A:代理因子(固定阈值)
hp2 = hp.kline.sma(window=20, price_htype='close')
hp2 = hp2.kline.macd(price_htype='close')
hp2 = hp2.kline.bbands(window=20, price_htype='close')
vals = hp2.values.astype(float)
close = vals[:, :, hp2.htypes.index('close')]
sma20 = vals[:, :, hp2.htypes.index('sma_20')]
macd_hist = vals[:, :, hp2.htypes.index('macd_hist_12_26_9')]
upper = vals[:, :, hp2.htypes.index('bbands_upper_20_2_2')]
mid = vals[:, :, hp2.htypes.index('bbands_middle_20_2_2')]
lower = vals[:, :, hp2.htypes.index('bbands_lower_20_2_2')]
value_proxy = close / sma20
momentum_proxy = macd_hist
risk_proxy = (upper - lower) / mid
A_VALUE = 1.02
C_MOM = 0.0
B_RISK = 0.18
cond_long = (value_proxy > A_VALUE) & (momentum_proxy > C_MOM) & (risk_proxy < B_RISK)
cond_short = (value_proxy < 1.0 / A_VALUE) & (momentum_proxy < -C_MOM) & (risk_proxy < B_RISK)
# 2) sanity check:每天入选数量
selected_count_by_day = cond_long.sum(axis=0)
print('\\n[Selection count stats]')
print(' min/max:', int(selected_count_by_day.min()), int(selected_count_by_day.max()))
print(' mean:', float(selected_count_by_day.mean()))
print(' p10/p50/p90:', np.quantile(selected_count_by_day.astype(float), [0.1, 0.5, 0.9]))
# 3) 条件 -> mask(研究口径)
mask_long = hp2.where(cond_long)
mask_short = hp2.where(cond_short)
# 4) 组合聚合 + benchmark
pf_long = hp2.portfolio(
htypes='close',
mode='equal',
mask=mask_long,
benchmark=benchmark,
benchmark_output='tag_along',
new_share_name='LONG',
)
pf_short = hp2.portfolio(
htypes='close',
mode='equal',
mask=mask_short,
benchmark=benchmark,
benchmark_output='tag_along',
new_share_name='SHORT',
)
# 5) 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)
# 6) 图:组合对比(归一化更直观)
fig = pf_long.normalize(htypes='close', base_index=0).plot(interactive=True, highlight='max')
return {
'hp': hp,
'hp2': hp2,
'pf_long': pf_long,
'pf_short': pf_short,
'summary': summary,
'fig': fig,
}
res = demo_horizontal_multifactor(
shares=[
'000001.SZ', '600519.SH', '300750.SZ', '000333.SZ', '600036.SH',
'601318.SH', '002415.SZ', '000858.SZ', '600276.SH', '000725.SZ',
'000300.SH',
],
benchmark='000300.SH',
start='20220101',
end='20221231',
)
res['fig']
5.11. 9. 小结与边界
An diesem Punkt haben wir die Forschungsschleife für die „Querschnitts-Multifaktor-Aktienauswahl“ abgeschlossen. Ich muss noch einmal betonen: „portfolio/cum_return“ ist eine forschungsorientierte grobe Aggregation und beinhaltet keine echte Handelsausführungssemantik. Wenn Sie diese Screening-Logik in einen Strategie-Backtest migrieren möchten, wird empfohlen, die Bedingungen/Faktoren als Strategiesignale auszugeben und sie an „Operator/Backtester“ zu übergeben, um die Details der Handelsebene zu verwalten.
5.12. Anhang: Abbildungsindex (empfohlen zur Erstellung/Screenshot im Notebook)
Abbildung |
Empfohlene Platzierung |
Was Sie sehen werden |
|---|---|---|
|
§0 |
Das minimal ausführbare Diagramm mit mehr Anteilen |
|
§0.5 oder §6 |
Normalisierter Kurvenvergleich von |
|
§7 |
Beispiel für die Hervorhebung eines „Schlüsselpunkts“ (z. B. des Maximalpunkts) |