siat 3.10.133__py3-none-any.whl → 3.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- siat/allin.py +8 -0
- siat/capm_beta2.py +4 -4
- siat/common.py +9 -6
- siat/grafix.py +55 -4
- siat/markowitz2.py +1 -0
- siat/risk_adjusted_return2.py +8 -4
- siat/save2docx.py +345 -0
- siat/save2pdf.py +145 -0
- siat/security_prices.py +128 -4
- siat/security_trend2.py +2 -2
- siat/stock.py +11 -1
- {siat-3.10.133.dist-info → siat-3.11.1.dist-info}/METADATA +4 -2
- {siat-3.10.133.dist-info → siat-3.11.1.dist-info}/RECORD +16 -14
- {siat-3.10.133.dist-info → siat-3.11.1.dist-info}/LICENSE +0 -0
- {siat-3.10.133.dist-info → siat-3.11.1.dist-info}/WHEEL +0 -0
- {siat-3.10.133.dist-info → siat-3.11.1.dist-info}/top_level.txt +0 -0
siat/allin.py
CHANGED
siat/capm_beta2.py
CHANGED
@@ -341,7 +341,7 @@ def compare_mticker_1beta(ticker,start,end, \
|
|
341
341
|
attention_point='',attention_point_area='', \
|
342
342
|
axhline_value=1,axhline_label='零线', \
|
343
343
|
band_area='', \
|
344
|
-
graph=True,facecolor='whitesmoke',loc='best', \
|
344
|
+
graph=True,facecolor='whitesmoke',loc='best',power=0, \
|
345
345
|
annotate=False,annotate_value=False, \
|
346
346
|
mark_top=False,mark_bottom=False, \
|
347
347
|
mark_start=False,mark_end=False, \
|
@@ -451,7 +451,7 @@ def compare_mticker_1beta(ticker,start,end, \
|
|
451
451
|
annotate=annotate,annotate_value=annotate, \
|
452
452
|
mark_top=mark_top,mark_bottom=mark_bottom, \
|
453
453
|
mark_start=mark_start,mark_end=mark_end, \
|
454
|
-
facecolor=facecolor,loc=loc,precision=4)
|
454
|
+
facecolor=facecolor,loc=loc,precision=4,power=power)
|
455
455
|
|
456
456
|
return df
|
457
457
|
|
@@ -786,7 +786,7 @@ def compare_beta_security(ticker,start,end, \
|
|
786
786
|
attention_value='',attention_value_area='', \
|
787
787
|
attention_point='',attention_point_area='', \
|
788
788
|
band_area='', \
|
789
|
-
graph=True,facecolor='whitesmoke', \
|
789
|
+
graph=True,power=0,facecolor='whitesmoke', \
|
790
790
|
annotate=False,annotate_value=False, \
|
791
791
|
mark_top=False,mark_bottom=False, \
|
792
792
|
mark_start=False,mark_end=False, \
|
@@ -867,7 +867,7 @@ def compare_beta_security(ticker,start,end, \
|
|
867
867
|
attention_value=attention_value,attention_value_area=attention_value_area, \
|
868
868
|
attention_point=attention_point,attention_point_area=attention_point_area, \
|
869
869
|
band_area=band_area, \
|
870
|
-
graph=graph,facecolor=facecolor,loc=loc, \
|
870
|
+
graph=graph,power=power,facecolor=facecolor,loc=loc, \
|
871
871
|
annotate=annotate,annotate_value=annotate, \
|
872
872
|
mark_top=mark_top,mark_bottom=mark_bottom, \
|
873
873
|
mark_start=mark_start,mark_end=mark_end, \
|
siat/common.py
CHANGED
@@ -5105,7 +5105,8 @@ async def jupyter2pdf3(notebook_path):
|
|
5105
5105
|
|
5106
5106
|
# pdf文件的完整路径
|
5107
5107
|
output_pdf_path1=notebook_dir+sep+notebook_file1+' A4.pdf'
|
5108
|
-
output_pdf_path2=notebook_dir+sep+notebook_file1+' A3.pdf'
|
5108
|
+
#output_pdf_path2=notebook_dir+sep+notebook_file1+' A3.pdf'
|
5109
|
+
output_pdf_path2=notebook_dir+sep+notebook_file1+'.pdf'
|
5109
5110
|
|
5110
5111
|
from nbconvert import HTMLExporter
|
5111
5112
|
|
@@ -5118,7 +5119,7 @@ async def jupyter2pdf3(notebook_path):
|
|
5118
5119
|
from playwright.async_api import async_playwright
|
5119
5120
|
#from playwright.sync_api import sync_playwright
|
5120
5121
|
except:
|
5121
|
-
print(" #Warning(
|
5122
|
+
print(" #Warning(jupyter2pdf3): playwright seems not fully installed yet")
|
5122
5123
|
print(" [Solution] execute the command before re-run: playwright install")
|
5123
5124
|
return
|
5124
5125
|
|
@@ -5129,10 +5130,11 @@ async def jupyter2pdf3(notebook_path):
|
|
5129
5130
|
html_exporter = HTMLExporter()
|
5130
5131
|
try:
|
5131
5132
|
html_content, _ = html_exporter.from_filename(notebook_path)
|
5132
|
-
print(f"Converting {notebook_file} to pdf ...")
|
5133
|
+
#print(f"Converting {notebook_file} to pdf ...")
|
5134
|
+
print(f"Converting from {notebook_file} ...")
|
5133
5135
|
|
5134
5136
|
except:
|
5135
|
-
print("
|
5137
|
+
print("Source file not found from {}".format(notebook_path))
|
5136
5138
|
return
|
5137
5139
|
|
5138
5140
|
# 创建临时 HTML 文件
|
@@ -5150,7 +5152,7 @@ async def jupyter2pdf3(notebook_path):
|
|
5150
5152
|
if not sys.platform.startswith('win'):
|
5151
5153
|
page.wait_for_selector(".jp-Notebook", state="visible", timeout=60000) # 等待笔记本主体出现
|
5152
5154
|
|
5153
|
-
await page.pdf(path=output_pdf_path1, format='A4')
|
5155
|
+
#await page.pdf(path=output_pdf_path1, format='A4')
|
5154
5156
|
await page.pdf(path=output_pdf_path2, format='A3')
|
5155
5157
|
|
5156
5158
|
await browser.close()
|
@@ -5170,7 +5172,8 @@ async def jupyter2pdf3(notebook_path):
|
|
5170
5172
|
browser.close()
|
5171
5173
|
"""
|
5172
5174
|
|
5173
|
-
print(f"2 PDFs created in {notebook_dir}")
|
5175
|
+
#print(f"2 PDFs created in {notebook_dir}")
|
5176
|
+
print(f"PDF created in {notebook_dir}")
|
5174
5177
|
|
5175
5178
|
except Exception as e:
|
5176
5179
|
if str(e)=='':
|
siat/grafix.py
CHANGED
@@ -276,7 +276,7 @@ def plot_line(df0,colname,collabel,ylabeltxt,titletxt,footnote,datatag=False, \
|
|
276
276
|
plt.axhline(y=hline,ls=":",c="black",linewidth=2,label='')
|
277
277
|
haveLegend=False
|
278
278
|
else:
|
279
|
-
|
279
|
+
#不再必要,被attention_value的逻辑替代
|
280
280
|
if isinstance(zeroline,float) or isinstance(zeroline,int):
|
281
281
|
hline=zeroline
|
282
282
|
plt.axhline(y=hline,ls=":",c="darkorange",linewidth=3,label=text_lang("关注值","Attention"))
|
@@ -373,7 +373,7 @@ def plot_line(df0,colname,collabel,ylabeltxt,titletxt,footnote,datatag=False, \
|
|
373
373
|
#print("--Debug(plot_line): power=",power)
|
374
374
|
if power > 0:
|
375
375
|
trend_txt=text_lang('趋势线','Trend line')
|
376
|
-
|
376
|
+
if power > 100: power=100
|
377
377
|
try:
|
378
378
|
#生成行号,借此将横轴的日期数量化,以便拟合
|
379
379
|
df['id']=range(len(df))
|
@@ -383,6 +383,9 @@ def plot_line(df0,colname,collabel,ylabeltxt,titletxt,footnote,datatag=False, \
|
|
383
383
|
parameter = np.polyfit(df.id, df[colname], power)
|
384
384
|
f = np.poly1d(parameter)
|
385
385
|
plt.plot(df.index, f(df.id),"r--", label=trend_txt,linewidth=1)
|
386
|
+
|
387
|
+
|
388
|
+
|
386
389
|
haveLegend=True
|
387
390
|
except:
|
388
391
|
print(" #Warning(plot_line): failed to converge trend line, try a smaller power.")
|
@@ -484,6 +487,8 @@ def plot_line2(df1,ticker1,colname1,label1, \
|
|
484
487
|
print("Going to plot_line2_coaxial")
|
485
488
|
print("yline=",yline,"; xline=",xline)
|
486
489
|
|
490
|
+
if power > 100: power=100
|
491
|
+
|
487
492
|
#if not twinx:
|
488
493
|
if twinx == True: # 双轴会图
|
489
494
|
plot_line2_twinx(df1,ticker1,colname1,label1, \
|
@@ -579,6 +584,8 @@ def plot2_line2(df1,ticker1,colname1,label1, \
|
|
579
584
|
if (len(df1) ==0) and (len(df2) ==0):
|
580
585
|
return
|
581
586
|
|
587
|
+
if power > 100: power=100
|
588
|
+
|
582
589
|
if not twinx:
|
583
590
|
plot_line2_coaxial2(df1,ticker1,colname1,label1, \
|
584
591
|
df2,ticker2,colname2,label2, \
|
@@ -2176,7 +2183,7 @@ def draw_lines(df0,y_label,x_label,axhline_value,axhline_label,title_txt, \
|
|
2176
2183
|
ticker_type='auto',facecolor='whitesmoke', \
|
2177
2184
|
maxticks_enable=True,maxticks=15, \
|
2178
2185
|
translate=False, \
|
2179
|
-
precision=2):
|
2186
|
+
precision=2,power=0):
|
2180
2187
|
"""
|
2181
2188
|
函数功能:根据df的内容绘制折线图
|
2182
2189
|
输入参数:
|
@@ -2308,8 +2315,22 @@ def draw_lines(df0,y_label,x_label,axhline_value,axhline_label,title_txt, \
|
|
2308
2315
|
mark_end=False
|
2309
2316
|
df_end=dfg.tail(1)
|
2310
2317
|
# df_end[c]必须为数值类型,否则可能出错
|
2318
|
+
if DEBUG:
|
2319
|
+
print(f"=== c: {type(c)}, {c}")
|
2320
|
+
print(f"=== df_end[c]: {type(df_end[c])}, {df_end[c]}")
|
2321
|
+
|
2311
2322
|
y_end = df_end[c].min() # 末端的y坐标
|
2323
|
+
if not isinstance(y_end,float):
|
2324
|
+
# 需要强制提取数值,因其可能为Series类型
|
2325
|
+
print(f"=== y_end: {type(y_end)}, {y_end}")
|
2326
|
+
y_end = y_end.iloc[0]
|
2327
|
+
|
2312
2328
|
x_end = df_end[c].idxmin() # 末端值的x坐标
|
2329
|
+
import pandas as pd
|
2330
|
+
if not isinstance(x_end,pd.Timestamp):
|
2331
|
+
# 需要强制提取数值,因其可能为Series类型
|
2332
|
+
print(f"=== x_end: {type(x_end)}, {x_end}")
|
2333
|
+
x_end = x_end.iloc[0]
|
2313
2334
|
|
2314
2335
|
if annotate_value: #在标记曲线名称的同时标记其末端数值
|
2315
2336
|
#y1=str(int(y_end)) if y_end >= 100 else str(round(y_end,2))
|
@@ -2386,7 +2407,37 @@ def draw_lines(df0,y_label,x_label,axhline_value,axhline_label,title_txt, \
|
|
2386
2407
|
xy=(x_end, y_end),
|
2387
2408
|
xytext=(x_end, y_end*0.998),fontsize=annotate_size,
|
2388
2409
|
color=last_line_color,ha='left',va='center')
|
2389
|
-
|
2410
|
+
|
2411
|
+
#绘制趋势线
|
2412
|
+
if (power > 0) and (len(list(dfg)) == 1):
|
2413
|
+
trend_txt=text_lang('趋势线','Trend line')
|
2414
|
+
if power > 100: power=100
|
2415
|
+
|
2416
|
+
try:
|
2417
|
+
#生成行号,借此将横轴的日期数量化,以便拟合
|
2418
|
+
dfg['id']=range(len(dfg))
|
2419
|
+
|
2420
|
+
#设定多项式拟合,power为多项式次数
|
2421
|
+
"""
|
2422
|
+
import numpy as np
|
2423
|
+
parameter = np.polyfit(dfg.id, dfg[list(dfg)[0]], power)
|
2424
|
+
f = np.poly1d(parameter)
|
2425
|
+
plt.plot(dfg.index, f(dfg.id),"r--", label=trend_txt,linewidth=1)
|
2426
|
+
"""
|
2427
|
+
from numpy.polynomial import Polynomial
|
2428
|
+
x = dfg['id']
|
2429
|
+
y = dfg[list(dfg)[0]]
|
2430
|
+
p = Polynomial.fit(x, y, deg=power)
|
2431
|
+
plt.plot(dfg.index, p(x), "r--", label=trend_txt, linewidth=1)
|
2432
|
+
|
2433
|
+
except Exception as e:
|
2434
|
+
print(f" #Warning(draw_lines): Polynomial.fit failed — {e}")
|
2435
|
+
|
2436
|
+
"""
|
2437
|
+
except:
|
2438
|
+
print(f" #Warning(draw_lines): failed to converge trend line, try a smaller power.")
|
2439
|
+
"""
|
2440
|
+
|
2390
2441
|
#用于关注值的颜色列表
|
2391
2442
|
atv_color_list=["lightgray","paleturquoise","wheat","khaki","lightsage","hotpink","mediumslateblue"]
|
2392
2443
|
#用于关注点的颜色列表
|
siat/markowitz2.py
CHANGED
siat/risk_adjusted_return2.py
CHANGED
@@ -813,7 +813,7 @@ def compare_1ticker_mrar(ticker,start,end,rar=['sharpe','sortino','treynor','alp
|
|
813
813
|
attention_point='',attention_point_area='', \
|
814
814
|
band_area='', \
|
815
815
|
graph=True,loc1='best', \
|
816
|
-
axhline_value=0,axhline_label='',facecolor='whitesmoke', \
|
816
|
+
axhline_value=0,axhline_label='',power=0,facecolor='whitesmoke', \
|
817
817
|
printout=False,sortby='tpw_mean',trailing=7,trend_threshhold=0.01, \
|
818
818
|
annotate=False,annotate_value=False, \
|
819
819
|
mark_top=False,mark_bottom=False, \
|
@@ -917,6 +917,10 @@ def compare_1ticker_mrar(ticker,start,end,rar=['sharpe','sortino','treynor','alp
|
|
917
917
|
x_label=text_lang("数据来源: 综合新浪/EM/Stooq/Yahoo/SWHY,","Data source: Sina/Stooq/Yahoo, ")+str(todaydt)
|
918
918
|
title_txt=text_lang("风险调整收益:","Risk-adjusted Return: ")+tname
|
919
919
|
|
920
|
+
# 英文环境下将label首字母大写
|
921
|
+
for c in list(df1):
|
922
|
+
df1.rename(columns={c:c.title()},inplace=True)
|
923
|
+
|
920
924
|
draw_lines(df1,y_label,x_label=footnotex, \
|
921
925
|
axhline_value=axhline_value,axhline_label=axhline_label, \
|
922
926
|
title_txt=title_txt,data_label=False, \
|
@@ -926,7 +930,7 @@ def compare_1ticker_mrar(ticker,start,end,rar=['sharpe','sortino','treynor','alp
|
|
926
930
|
band_area=band_area, \
|
927
931
|
mark_top=mark_top,mark_bottom=mark_bottom, \
|
928
932
|
mark_start=mark_start,mark_end=mark_end, \
|
929
|
-
facecolor=facecolor,loc=loc1)
|
933
|
+
facecolor=facecolor,loc=loc1,power=power)
|
930
934
|
|
931
935
|
#制表
|
932
936
|
recommenddf=pd.DataFrame()
|
@@ -1729,7 +1733,7 @@ def compare_rar_security(ticker,start,end='today',indicator='sharpe', \
|
|
1729
1733
|
attention_point='',attention_point_area='', \
|
1730
1734
|
band_area='', \
|
1731
1735
|
graph=True,loc1='best', \
|
1732
|
-
axhline_value=0,axhline_label='',facecolor='whitesmoke', \
|
1736
|
+
axhline_value=0,axhline_label='',power=0,facecolor='whitesmoke', \
|
1733
1737
|
printout=False,sortby='tpw_mean',trailing=7,trend_threshhold=0.05, \
|
1734
1738
|
annotate=False,annotate_value=False, \
|
1735
1739
|
mark_top=False,mark_bottom=False, \
|
@@ -1878,7 +1882,7 @@ def compare_rar_security(ticker,start,end='today',indicator='sharpe', \
|
|
1878
1882
|
attention_value=attention_value,attention_value_area=attention_value_area, \
|
1879
1883
|
attention_point=attention_point,attention_point_area=attention_point_area, \
|
1880
1884
|
graph=graph,loc1=loc1, \
|
1881
|
-
axhline_value=axhline_value,axhline_label=axhline_label, \
|
1885
|
+
axhline_value=axhline_value,axhline_label=axhline_label,power=power, \
|
1882
1886
|
printout=printout,sortby=sortby, \
|
1883
1887
|
trailing=trailing,trend_threshhold=trend_threshhold, \
|
1884
1888
|
annotate=annotate,annotate_value=annotate, \
|
siat/save2docx.py
ADDED
@@ -0,0 +1,345 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
本模块功能:转换ipynb文件为docx,带有的目录,代码行加边框,图像适配页宽。
|
4
|
+
注意:
|
5
|
+
需要安装pandoc并将其路径加入操作系统的PATH。
|
6
|
+
可在Anaconda Prompt或macOS Terminal下输入pandoc尝试,若未加入PATH则提示找不到。
|
7
|
+
尚存问题:
|
8
|
+
1. 标题行未居中,且重复生成;
|
9
|
+
2. 目录页码不准确,需要手动更新;
|
10
|
+
3. 若docx文件已打开出错。
|
11
|
+
所属工具包:证券投资分析工具SIAT
|
12
|
+
SIAT:Security Investment Analysis Tool
|
13
|
+
创建日期:2025年7月8日
|
14
|
+
最新修订日期:2025年7月8日
|
15
|
+
作者:王德宏 (WANG Dehong, Peter)
|
16
|
+
作者单位:北京外国语大学国际商学院
|
17
|
+
作者邮件:wdehong2000@163.com
|
18
|
+
版权所有:王德宏
|
19
|
+
用途限制:仅限研究与教学使用。
|
20
|
+
特别声明:作者不对使用本工具进行证券投资导致的任何损益负责!
|
21
|
+
"""
|
22
|
+
|
23
|
+
#==============================================================================
|
24
|
+
|
25
|
+
import os
|
26
|
+
import errno
|
27
|
+
import tempfile
|
28
|
+
import subprocess
|
29
|
+
|
30
|
+
import nbformat
|
31
|
+
from nbconvert import HTMLExporter
|
32
|
+
import pypandoc
|
33
|
+
|
34
|
+
from docx import Document
|
35
|
+
from docx.oxml import OxmlElement
|
36
|
+
from docx.oxml.ns import qn
|
37
|
+
from docx.shared import Mm
|
38
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
39
|
+
from docx.enum.table import WD_TABLE_ALIGNMENT
|
40
|
+
from docx.text.paragraph import Paragraph
|
41
|
+
|
42
|
+
# 预设纸张尺寸(单位:毫米)
|
43
|
+
PAGE_SIZES = {"A4": (210, 297), "A3": (297, 420)}
|
44
|
+
|
45
|
+
|
46
|
+
def _add_border_to_paragraph(paragraph):
|
47
|
+
"""给 paragraph 添加四边单线边框"""
|
48
|
+
p = paragraph._p
|
49
|
+
pPr = p.get_or_add_pPr()
|
50
|
+
pBdr = OxmlElement('w:pBdr')
|
51
|
+
for edge in ('top', 'left', 'bottom', 'right'):
|
52
|
+
elm = OxmlElement(f'w:{edge}')
|
53
|
+
elm.set(qn('w:val'), 'single')
|
54
|
+
elm.set(qn('w:sz'), '4')
|
55
|
+
elm.set(qn('w:space'), '4')
|
56
|
+
elm.set(qn('w:color'), 'auto')
|
57
|
+
pBdr.append(elm)
|
58
|
+
pPr.append(pBdr)
|
59
|
+
|
60
|
+
|
61
|
+
def _insert_native_toc(after_paragraph):
|
62
|
+
"""
|
63
|
+
在 after_paragraph 之后插入一个 Word 原生 TOC 域,
|
64
|
+
支持更新标题和页码,涵盖级别 1–9。
|
65
|
+
"""
|
66
|
+
# 构造 <w:p> 节点
|
67
|
+
toc_p = OxmlElement('w:p')
|
68
|
+
|
69
|
+
# 1) fldChar begin
|
70
|
+
r1 = OxmlElement('w:r')
|
71
|
+
fld_char_begin = OxmlElement('w:fldChar')
|
72
|
+
fld_char_begin.set(qn('w:fldCharType'), 'begin')
|
73
|
+
r1.append(fld_char_begin)
|
74
|
+
toc_p.append(r1)
|
75
|
+
|
76
|
+
# 2) instrText
|
77
|
+
r2 = OxmlElement('w:r')
|
78
|
+
instr = OxmlElement('w:instrText')
|
79
|
+
instr.set(qn('xml:space'), 'preserve')
|
80
|
+
instr.text = 'TOC \\o "1-9" \\h \\z \\u'
|
81
|
+
r2.append(instr)
|
82
|
+
toc_p.append(r2)
|
83
|
+
|
84
|
+
# 3) fldChar separate
|
85
|
+
r3 = OxmlElement('w:r')
|
86
|
+
fld_char_sep = OxmlElement('w:fldChar')
|
87
|
+
fld_char_sep.set(qn('w:fldCharType'), 'separate')
|
88
|
+
r3.append(fld_char_sep)
|
89
|
+
toc_p.append(r3)
|
90
|
+
|
91
|
+
# 4) 占位文本(可选)
|
92
|
+
r4 = OxmlElement('w:r')
|
93
|
+
t = OxmlElement('w:t')
|
94
|
+
t.text = '右击此处更新目录'
|
95
|
+
r4.append(t)
|
96
|
+
toc_p.append(r4)
|
97
|
+
|
98
|
+
# 5) fldChar end
|
99
|
+
r5 = OxmlElement('w:r')
|
100
|
+
fld_char_end = OxmlElement('w:fldChar')
|
101
|
+
fld_char_end.set(qn('w:fldCharType'), 'end')
|
102
|
+
r5.append(fld_char_end)
|
103
|
+
toc_p.append(r5)
|
104
|
+
|
105
|
+
# 插入
|
106
|
+
after_paragraph._p.addnext(toc_p)
|
107
|
+
|
108
|
+
|
109
|
+
def convert_ipynb_to_docx(ipynb_path, docx_path=None, page_size="A3"):
|
110
|
+
"""
|
111
|
+
将 .ipynb 转为 .docx,并实现:
|
112
|
+
1. 第一 Markdown 单元首行做文档标题
|
113
|
+
2. 在第2行插入可更新的 Word 原生 TOC(1–9 级)
|
114
|
+
3. 所有标题左对齐
|
115
|
+
4. 仅给原始 code 单元段落加边框(不含输出)
|
116
|
+
5. 表格等分列宽居中;图像放大至页宽并居中
|
117
|
+
6. 若目标 docx 正被打开,抛出提示“请先关闭文件”
|
118
|
+
"""
|
119
|
+
# ---- 1. 检查输入 & 输出路径 ----
|
120
|
+
if not os.path.isfile(ipynb_path):
|
121
|
+
raise FileNotFoundError(f"找不到输入文件:{ipynb_path}")
|
122
|
+
if docx_path is None:
|
123
|
+
base, _ = os.path.splitext(ipynb_path)
|
124
|
+
docx_path = base + ".docx"
|
125
|
+
|
126
|
+
# ---- 2. 读取 Notebook,提取标题 & 收集 code 单元源码 ----
|
127
|
+
nb = nbformat.read(ipynb_path, as_version=4)
|
128
|
+
title = None
|
129
|
+
code_blocks = []
|
130
|
+
for cell in nb.cells:
|
131
|
+
if cell.cell_type == "markdown" and title is None:
|
132
|
+
lines = cell.source.strip().splitlines()
|
133
|
+
if lines:
|
134
|
+
title = lines[0].lstrip("# ").strip()
|
135
|
+
# 去除这行,避免后面重复
|
136
|
+
cell.source = "\n".join(lines[1:]).strip()
|
137
|
+
if cell.cell_type == "code":
|
138
|
+
code_blocks.append(cell.source.rstrip())
|
139
|
+
if not title:
|
140
|
+
title = os.path.splitext(os.path.basename(ipynb_path))[0]
|
141
|
+
|
142
|
+
# ---- 3. 确保 Pandoc 可用 ----
|
143
|
+
try:
|
144
|
+
pypandoc.get_pandoc_version()
|
145
|
+
except OSError:
|
146
|
+
pypandoc.download_pandoc()
|
147
|
+
|
148
|
+
# ---- 4. Notebook → HTML(嵌入图像) ----
|
149
|
+
exporter = HTMLExporter()
|
150
|
+
exporter.embed_images = True
|
151
|
+
html_body, _ = exporter.from_notebook_node(nb)
|
152
|
+
html = f"<h1>{title}</h1>\n" + html_body
|
153
|
+
|
154
|
+
# ---- 5. HTML → DOCX via Pandoc(或 subprocess fallback) ----
|
155
|
+
try:
|
156
|
+
pypandoc.convert_text(
|
157
|
+
html, to="docx", format="html",
|
158
|
+
outputfile=docx_path, encoding="utf-8"
|
159
|
+
)
|
160
|
+
except Exception:
|
161
|
+
# fallback 到外部 pandoc
|
162
|
+
with tempfile.NamedTemporaryFile("w", suffix=".html",
|
163
|
+
delete=False,
|
164
|
+
encoding="utf-8") as tmp:
|
165
|
+
tmp.write(html)
|
166
|
+
html_file = tmp.name
|
167
|
+
try:
|
168
|
+
try:
|
169
|
+
subprocess.run(
|
170
|
+
["pandoc", html_file, "-f", "html", "-t", "docx", "-o", docx_path],
|
171
|
+
check=True, capture_output=True
|
172
|
+
)
|
173
|
+
except subprocess.CalledProcessError as e:
|
174
|
+
err = e.stderr.decode("utf-8", errors="ignore")
|
175
|
+
low = err.lower()
|
176
|
+
if "permission denied" in low or "could not open file" in low:
|
177
|
+
raise PermissionError(
|
178
|
+
f"无法写入 {docx_path}:文件可能已被打开,请先关闭后重试。"
|
179
|
+
)
|
180
|
+
raise RuntimeError(f"Pandoc 转换失败:{err}")
|
181
|
+
finally:
|
182
|
+
os.remove(html_file)
|
183
|
+
|
184
|
+
# ---- 6. 后处理 DOCX ----
|
185
|
+
try:
|
186
|
+
doc = Document(docx_path)
|
187
|
+
except (PermissionError, OSError) as e:
|
188
|
+
if getattr(e, "errno", None) in (errno.EACCES, errno.EPERM):
|
189
|
+
raise PermissionError(
|
190
|
+
f"无法打开 {docx_path}:文件可能已被打开,请先关闭后重试。"
|
191
|
+
)
|
192
|
+
raise
|
193
|
+
|
194
|
+
# 6.1 页面尺寸 & 边距
|
195
|
+
sec = doc.sections[0]
|
196
|
+
if page_size.upper() in PAGE_SIZES:
|
197
|
+
w_mm, h_mm = PAGE_SIZES[page_size.upper()]
|
198
|
+
sec.page_width, sec.page_height = Mm(w_mm), Mm(h_mm)
|
199
|
+
for m in ("left_margin", "right_margin", "top_margin", "bottom_margin"):
|
200
|
+
setattr(sec, m, Mm(25.4))
|
201
|
+
avail_w = sec.page_width - sec.left_margin - sec.right_margin
|
202
|
+
|
203
|
+
# 6.2 第一段替换为标题,设为 Heading1,左对齐
|
204
|
+
p0 = doc.paragraphs[0]
|
205
|
+
p0.text = title
|
206
|
+
p0.style = doc.styles["Heading 1"]
|
207
|
+
p0.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
208
|
+
|
209
|
+
# 6.3 在第2行插入 Word 本地 TOC
|
210
|
+
_insert_native_toc(p0)
|
211
|
+
|
212
|
+
# 6.4 强制 Word 打开时自动更新目录
|
213
|
+
try:
|
214
|
+
settings = doc.settings.element
|
215
|
+
upd = OxmlElement("w:updateFields")
|
216
|
+
upd.set(qn("w:val"), "true")
|
217
|
+
settings.append(upd)
|
218
|
+
except Exception:
|
219
|
+
pass # 部分 python-docx 版本无此接口
|
220
|
+
|
221
|
+
# 6.5 所有标题左对齐
|
222
|
+
for p in doc.paragraphs:
|
223
|
+
if p.style.name.startswith("Heading"):
|
224
|
+
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
225
|
+
|
226
|
+
# 6.6 仅给原始 code 单元对应段落加边框
|
227
|
+
for p in doc.paragraphs:
|
228
|
+
if "code" in p.style.name.lower():
|
229
|
+
txt = p.text.rstrip()
|
230
|
+
if any(txt == block for block in code_blocks):
|
231
|
+
_add_border_to_paragraph(p)
|
232
|
+
|
233
|
+
# 6.7 表格等分列宽并居中
|
234
|
+
for tbl in doc.tables:
|
235
|
+
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
|
236
|
+
tbl.allow_autofit = False
|
237
|
+
cols = len(tbl.columns) or 1
|
238
|
+
col_w = avail_w // cols
|
239
|
+
for col in tbl.columns:
|
240
|
+
for cell in col.cells:
|
241
|
+
cell.width = col_w
|
242
|
+
|
243
|
+
# 6.8 图像放大至页宽并居中
|
244
|
+
for shp in doc.inline_shapes:
|
245
|
+
ow, oh = shp.width, shp.height
|
246
|
+
fact = avail_w / ow
|
247
|
+
shp.width = avail_w
|
248
|
+
shp.height = int(oh * fact)
|
249
|
+
p_el = shp._inline.getparent().getparent()
|
250
|
+
Paragraph(p_el, doc).alignment = WD_ALIGN_PARAGRAPH.CENTER
|
251
|
+
|
252
|
+
# ---- 7. 保存并捕捉写入锁定 ----
|
253
|
+
try:
|
254
|
+
doc.save(docx_path)
|
255
|
+
except (PermissionError, OSError) as e:
|
256
|
+
if getattr(e, "errno", None) in (errno.EACCES, errno.EPERM):
|
257
|
+
raise PermissionError(
|
258
|
+
f"无法写入 {docx_path}:文件可能已被打开,请先关闭后重试。"
|
259
|
+
)
|
260
|
+
raise
|
261
|
+
|
262
|
+
return docx_path
|
263
|
+
|
264
|
+
|
265
|
+
#==============================================================================
|
266
|
+
import os
|
267
|
+
import sys
|
268
|
+
import psutil
|
269
|
+
|
270
|
+
def is_file_opened(file_path: str) -> bool:
|
271
|
+
"""
|
272
|
+
检测文件是否被其他程序打开(跨平台)
|
273
|
+
:param file_path: 文件路径
|
274
|
+
:return: True-被占用, False-未占用或不存在
|
275
|
+
"""
|
276
|
+
# 检查文件是否存在
|
277
|
+
if not os.path.exists(file_path):
|
278
|
+
return False
|
279
|
+
|
280
|
+
abs_path = os.path.abspath(file_path) # 转为绝对路径
|
281
|
+
|
282
|
+
# 方法1:异常捕获法(快速检测)
|
283
|
+
try:
|
284
|
+
with open(abs_path, "a") as f: # 追加模式(不破坏内容)
|
285
|
+
pass
|
286
|
+
return False # 成功打开说明未被占用
|
287
|
+
except (OSError, PermissionError):
|
288
|
+
pass # 继续尝试其他方法
|
289
|
+
|
290
|
+
# 方法2:进程扫描法(精确检测)
|
291
|
+
try:
|
292
|
+
for proc in psutil.process_iter(['pid', 'name', 'open_files']):
|
293
|
+
try:
|
294
|
+
open_files = proc.info.get('open_files')
|
295
|
+
if open_files and any(f.path == abs_path for f in open_files):
|
296
|
+
return True
|
297
|
+
except (psutil.AccessDenied, psutil.NoSuchProcess):
|
298
|
+
continue
|
299
|
+
except NameError: # psutil未安装
|
300
|
+
pass
|
301
|
+
|
302
|
+
# 方法3:文件锁试探法(最终回退)
|
303
|
+
try:
|
304
|
+
if sys.platform == 'win32':
|
305
|
+
import msvcrt
|
306
|
+
with open(abs_path, "a") as f:
|
307
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1) # 非阻塞锁
|
308
|
+
else:
|
309
|
+
import fcntl
|
310
|
+
with open(abs_path, "a") as f:
|
311
|
+
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) # 非阻塞独占锁
|
312
|
+
return False
|
313
|
+
except (OSError, BlockingIOError, ImportError):
|
314
|
+
return True # 所有检测均失败视为占用
|
315
|
+
return False
|
316
|
+
|
317
|
+
#==============================================================================
|
318
|
+
|
319
|
+
def ipynb2docx(ipynb_path, page_size="A3"):
|
320
|
+
"""
|
321
|
+
将 .ipynb 转为 .docx,特性:
|
322
|
+
1. Markdown 首行做文档标题
|
323
|
+
2. 在第 2 行插入全文 TOC(1–9 级)
|
324
|
+
3. 所有标题左对齐,保留原字号
|
325
|
+
4. 仅为“代码段”加边框,不影响输出
|
326
|
+
5. 表格均分列宽并居中
|
327
|
+
6. 图像放大至可用页宽并居中
|
328
|
+
7. 若目标文件已打开,捕获并提示“请先关闭文件”
|
329
|
+
"""
|
330
|
+
base, _ = os.path.splitext(ipynb_path)
|
331
|
+
docx_path = base + ".docx"
|
332
|
+
|
333
|
+
# 检测docx文件是否已被打开
|
334
|
+
if is_file_opened(docx_path):
|
335
|
+
print(f"Warning: {docx_path} occupied by other app, please close it and try again")
|
336
|
+
return
|
337
|
+
|
338
|
+
result = convert_ipynb_to_docx(ipynb_path, docx_path=None, page_size=page_size)
|
339
|
+
print(f"{result} created with TOC in {page_size} size")
|
340
|
+
print(f"However, TOC needs update manually in Microsoft Word")
|
341
|
+
print(f"And, title and some other things may need fine tuned as well")
|
342
|
+
|
343
|
+
return
|
344
|
+
|
345
|
+
|
siat/save2pdf.py
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
本模块功能:转换ipynb文件为pdf,带有可跳转的目录(目前一级标题定位还不准确,二级以下目录定位较准确,但已可用)
|
4
|
+
所属工具包:证券投资分析工具SIAT
|
5
|
+
SIAT:Security Investment Analysis Tool
|
6
|
+
创建日期:2025年7月8日
|
7
|
+
最新修订日期:2025年7月8日
|
8
|
+
作者:王德宏 (WANG Dehong, Peter)
|
9
|
+
作者单位:北京外国语大学国际商学院
|
10
|
+
作者邮件:wdehong2000@163.com
|
11
|
+
版权所有:王德宏
|
12
|
+
用途限制:仅限研究与教学使用。
|
13
|
+
特别声明:作者不对使用本工具进行证券投资导致的任何损益负责!
|
14
|
+
"""
|
15
|
+
|
16
|
+
#==============================================================================
|
17
|
+
|
18
|
+
# 首次运行前,请安装依赖:
|
19
|
+
# !pip install nbformat nbconvert playwright pymupdf nest_asyncio
|
20
|
+
# !playwright install
|
21
|
+
|
22
|
+
import os
|
23
|
+
import re
|
24
|
+
import tempfile
|
25
|
+
import asyncio
|
26
|
+
|
27
|
+
import nest_asyncio
|
28
|
+
import nbformat
|
29
|
+
from nbconvert import HTMLExporter
|
30
|
+
from playwright.async_api import async_playwright
|
31
|
+
import fitz # PyMuPDF
|
32
|
+
|
33
|
+
nest_asyncio.apply() # 使 asyncio.run 在 Notebook 中可用
|
34
|
+
|
35
|
+
def ipynb2pdf(ipynb_path: str) -> str:
|
36
|
+
"""
|
37
|
+
将 .ipynb 转为带可跳转目录书签的 PDF。
|
38
|
+
返回生成的 PDF 文件路径。
|
39
|
+
"""
|
40
|
+
if not os.path.isfile(ipynb_path):
|
41
|
+
raise FileNotFoundError(f"找不到文件:{ipynb_path}")
|
42
|
+
output_pdf = ipynb_path[:-6] + ".pdf"
|
43
|
+
|
44
|
+
# 1. 读 notebook → 提取目录结构
|
45
|
+
nb = nbformat.read(ipynb_path, as_version=4)
|
46
|
+
toc = _extract_toc(nb)
|
47
|
+
|
48
|
+
# 2. nb → HTML
|
49
|
+
exporter = HTMLExporter()
|
50
|
+
html_body, _ = exporter.from_notebook_node(nb)
|
51
|
+
|
52
|
+
# 3. 临时写 HTML / PDF
|
53
|
+
with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as th:
|
54
|
+
th.write(html_body)
|
55
|
+
html_path = th.name
|
56
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tp:
|
57
|
+
tmp_pdf = tp.name
|
58
|
+
|
59
|
+
# 4. Playwright 渲染 HTML → PDF
|
60
|
+
asyncio.run(_html_to_pdf(html_path, tmp_pdf))
|
61
|
+
|
62
|
+
# 5. PyMuPDF 添加书签
|
63
|
+
_add_bookmarks(tmp_pdf, output_pdf, toc)
|
64
|
+
|
65
|
+
# 6. 清理
|
66
|
+
os.unlink(html_path)
|
67
|
+
os.unlink(tmp_pdf)
|
68
|
+
|
69
|
+
from pathlib import Path
|
70
|
+
full_path = Path(output_pdf)
|
71
|
+
# 提取文件名
|
72
|
+
filename = full_path.name # 'report.pdf'
|
73
|
+
# 提取路径
|
74
|
+
directory = full_path.parent # PosixPath('/Users/peter/Documents')
|
75
|
+
|
76
|
+
print(f"✅ {filename} is created with TOC")
|
77
|
+
print(f"✅ It is in {directory}")
|
78
|
+
|
79
|
+
#return output_pdf
|
80
|
+
return
|
81
|
+
|
82
|
+
async def _html_to_pdf(html_path: str, pdf_path: str):
|
83
|
+
async with async_playwright() as p:
|
84
|
+
browser = await p.chromium.launch()
|
85
|
+
page = await browser.new_page()
|
86
|
+
await page.goto(f"file://{html_path}")
|
87
|
+
await page.pdf(
|
88
|
+
path=pdf_path,
|
89
|
+
#format="A4",
|
90
|
+
format="A3",
|
91
|
+
print_background=True,
|
92
|
+
margin={"top":"20mm","bottom":"20mm","left":"20mm","right":"20mm"},
|
93
|
+
)
|
94
|
+
await browser.close()
|
95
|
+
|
96
|
+
def _extract_toc(nb_node) -> list[tuple[int,str]]:
|
97
|
+
"""
|
98
|
+
从每个 markdown 单元首行提取 # 级别和标题文本,
|
99
|
+
返回 [(level, title), …]
|
100
|
+
"""
|
101
|
+
toc = []
|
102
|
+
for cell in nb_node.cells:
|
103
|
+
if cell.cell_type != "markdown":
|
104
|
+
continue
|
105
|
+
first = cell.source.strip().splitlines()[0]
|
106
|
+
m = re.match(r"^(#{1,6})\s+(.*)", first)
|
107
|
+
if m:
|
108
|
+
toc.append((len(m.group(1)), m.group(2).strip()))
|
109
|
+
return toc
|
110
|
+
|
111
|
+
def _add_bookmarks(input_pdf: str, output_pdf: str, toc: list[tuple[int,str]]):
|
112
|
+
"""
|
113
|
+
用 PyMuPDF 打开临时 PDF,按 toc 列表查找页码,
|
114
|
+
然后用 set_toc() 批量写入书签。
|
115
|
+
"""
|
116
|
+
doc = fitz.open(input_pdf)
|
117
|
+
outline = []
|
118
|
+
for level, title in toc:
|
119
|
+
page_num = 1
|
120
|
+
# 搜索标题出现在第几页(0-based → +1)
|
121
|
+
for i in range(doc.page_count):
|
122
|
+
if title in doc.load_page(i).get_text():
|
123
|
+
page_num = i + 1
|
124
|
+
break
|
125
|
+
outline.append([level, title, page_num])
|
126
|
+
|
127
|
+
# 批量设置目录书签
|
128
|
+
doc.set_toc(outline)
|
129
|
+
doc.save(output_pdf)
|
130
|
+
|
131
|
+
# 使用示例(另起一个 cell 运行):
|
132
|
+
# ipynb = globals().get("__session__")
|
133
|
+
# ipynb2pdf(ipynb)
|
134
|
+
|
135
|
+
|
136
|
+
#==============================================================================
|
137
|
+
|
138
|
+
#==============================================================================
|
139
|
+
#==============================================================================
|
140
|
+
#==============================================================================
|
141
|
+
#==============================================================================
|
142
|
+
#==============================================================================
|
143
|
+
#==============================================================================
|
144
|
+
#==============================================================================
|
145
|
+
#==============================================================================
|
siat/security_prices.py
CHANGED
@@ -2489,12 +2489,14 @@ if __name__ =="__main__":
|
|
2489
2489
|
vdf=rolling_ret_volatility(retdf, period)
|
2490
2490
|
|
2491
2491
|
#==============================================================================
|
2492
|
-
def
|
2492
|
+
def expanding_ret_volatility_x(df0,basedate):
|
2493
2493
|
"""
|
2494
2494
|
功能:基于日收益率数据集,从起始日期basedate开始的收益率波动风险扩展窗口序列。
|
2495
2495
|
输入:
|
2496
2496
|
日收益率数据集df。
|
2497
2497
|
输出:扩展调整收益率波动风险序列,按照日期升序排列。
|
2498
|
+
|
2499
|
+
注意:可能存在计算错误,暂时废弃!!!
|
2498
2500
|
"""
|
2499
2501
|
df0["Daily Ret"]=df0['Close'].pct_change()
|
2500
2502
|
df0["Daily Adj Ret"]=df0['Adj Close'].pct_change()
|
@@ -2523,6 +2525,59 @@ def expanding_ret_volatility(df0,basedate):
|
|
2523
2525
|
|
2524
2526
|
return df
|
2525
2527
|
|
2528
|
+
#==============================================================================
|
2529
|
+
def expanding_ret_volatility(df0,basedate,min_periods=1):
|
2530
|
+
"""
|
2531
|
+
功能:基于日收益率数据集,从起始日期basedate开始的收益率波动风险扩展窗口序列。
|
2532
|
+
输入:
|
2533
|
+
日收益率数据集df。
|
2534
|
+
输出:扩展调整收益率波动风险序列,按照日期升序排列。
|
2535
|
+
|
2536
|
+
新算法:解决开始部分过度波动问题
|
2537
|
+
"""
|
2538
|
+
collist=list(df0)
|
2539
|
+
|
2540
|
+
if not ("Daily Ret" in collist):
|
2541
|
+
df0["Daily Ret"]=df0['Close'].pct_change()
|
2542
|
+
#df0["Daily Ret"]=df0["Daily Ret"].fillna(method='bfill', axis=1)
|
2543
|
+
df0["Daily Ret"]=df0["Daily Ret"].interpolate()
|
2544
|
+
|
2545
|
+
if not ("Daily Adj Ret" in collist):
|
2546
|
+
df0["Daily Adj Ret"]=df0['Adj Close'].pct_change()
|
2547
|
+
#df0["Daily Adj Ret"]=df0["Daily Adj Ret"].fillna(method='bfill', axis=1)
|
2548
|
+
df0["Daily Adj Ret"]=df0["Daily Adj Ret"].interpolate()
|
2549
|
+
|
2550
|
+
import pandas as pd
|
2551
|
+
basedate_pd=pd.to_datetime(basedate)
|
2552
|
+
df=df0[df0.index >= basedate_pd]
|
2553
|
+
|
2554
|
+
# 计算Exp Ret和Exp Adj Ret
|
2555
|
+
if not ('Exp Ret' in collist):
|
2556
|
+
df['Exp Ret'] = (1 + df['Daily Ret']).cumprod() - 1
|
2557
|
+
df['Exp Ret%'] = df['Exp Ret'] * 100.0
|
2558
|
+
|
2559
|
+
if not ('Exp Adj Ret' in collist):
|
2560
|
+
df['Exp Adj Ret'] = (1 + df['Daily Adj Ret']).cumprod() - 1
|
2561
|
+
df['Exp Adj Ret%'] = df['Exp Adj Ret'] * 100.0
|
2562
|
+
|
2563
|
+
#计算扩展窗口调整收益率波动风险:基于普通收益率
|
2564
|
+
retname1="Exp Ret Volatility"
|
2565
|
+
retname2="Exp Ret Volatility%"
|
2566
|
+
#import numpy as np
|
2567
|
+
|
2568
|
+
#df[retname1]=df["Exp Ret"].expanding(min_periods=min_periods).std(ddof=1)
|
2569
|
+
df[retname1]=df["Exp Ret"].expanding().std(ddof=1)
|
2570
|
+
df[retname2]=df[retname1]*100.0
|
2571
|
+
|
2572
|
+
#计算扩展窗口调整收益率风险:基于调整收益率
|
2573
|
+
retname3="Exp Adj Ret Volatility"
|
2574
|
+
retname4="Exp Adj Ret Volatility%"
|
2575
|
+
#df[retname3]=df["Exp Adj Ret"].expanding(min_periods=min_periods).std(ddof=1)
|
2576
|
+
df[retname3]=df["Exp Adj Ret"].expanding().std(ddof=1)
|
2577
|
+
df[retname4]=df[retname3]*100.0
|
2578
|
+
|
2579
|
+
return df
|
2580
|
+
|
2526
2581
|
if __name__ =="__main__":
|
2527
2582
|
basedate='2019-1-1'
|
2528
2583
|
pricedf=get_price('000002.SZ','2018-1-1','2020-3-16')
|
@@ -2564,6 +2619,16 @@ if __name__ =="__main__":
|
|
2564
2619
|
df=get_price("000002.SZ","2020-1-1","2020-3-16")
|
2565
2620
|
print(lpsd(df['Close']))
|
2566
2621
|
|
2622
|
+
import numpy as np
|
2623
|
+
|
2624
|
+
def downside_std(returns, target_return=0):
|
2625
|
+
"""
|
2626
|
+
功能:计算下偏标准差(下方风险)
|
2627
|
+
注意:暂时弃用,因为容易引起float divided zero问题。
|
2628
|
+
"""
|
2629
|
+
downside_diff = np.maximum(target_return - returns, 0)
|
2630
|
+
return np.sqrt(np.mean(downside_diff ** 2))
|
2631
|
+
|
2567
2632
|
#==============================================================================
|
2568
2633
|
def rolling_ret_lpsd(df, period="Weekly"):
|
2569
2634
|
"""
|
@@ -2589,6 +2654,7 @@ def rolling_ret_lpsd(df, period="Weekly"):
|
|
2589
2654
|
retname2=retname1+'%'
|
2590
2655
|
#import numpy as np
|
2591
2656
|
df[retname1]=df[periodret].rolling(rollingnum,min_periods=1).apply(lambda x: lpsd(x))
|
2657
|
+
#df[retname1]=df[periodret].rolling(rollingnum,min_periods=1).apply(downside_std, raw=True)
|
2592
2658
|
df[retname2]=df[retname1]*100.0
|
2593
2659
|
|
2594
2660
|
#计算滚动下偏标准差:基于调整收益率
|
@@ -2596,6 +2662,7 @@ def rolling_ret_lpsd(df, period="Weekly"):
|
|
2596
2662
|
retname3=period+" Adj Ret LPSD"
|
2597
2663
|
retname4=retname3+'%'
|
2598
2664
|
df[retname3]=df[periodadjret].rolling(rollingnum,min_periods=1).apply(lambda x: lpsd(x))
|
2665
|
+
#df[retname3]=df[periodadjret].rolling(rollingnum,min_periods=1).apply(downside_std, raw=True)
|
2599
2666
|
df[retname4]=df[retname3]*100.0
|
2600
2667
|
|
2601
2668
|
return df
|
@@ -2607,12 +2674,14 @@ if __name__ =="__main__":
|
|
2607
2674
|
vdf=rolling_ret_lpsd(retdf, period)
|
2608
2675
|
|
2609
2676
|
#==============================================================================
|
2610
|
-
def
|
2677
|
+
def expanding_ret_lpsd_x(df0,basedate,min_periods=1):
|
2611
2678
|
"""
|
2612
2679
|
功能:基于日收益率数据集,从起始日期basedate开始的收益率损失风险扩展窗口序列。
|
2613
2680
|
输入:
|
2614
2681
|
日收益率数据集df。
|
2615
2682
|
输出:扩展调整收益率波动风险序列,按照日期升序排列。
|
2683
|
+
|
2684
|
+
注意:算法可能存在错误,暂时废弃!!!
|
2616
2685
|
"""
|
2617
2686
|
df0["Daily Ret"]=df0['Close'].pct_change()
|
2618
2687
|
df0["Daily Adj Ret"]=df0['Adj Close'].pct_change()
|
@@ -2626,7 +2695,7 @@ def expanding_ret_lpsd(df0,basedate):
|
|
2626
2695
|
retname2=retname1+'%'
|
2627
2696
|
import numpy as np
|
2628
2697
|
#df[retname1]=df["Daily Ret"].expanding(min_periods=1).apply(lambda x: lpsd(x)*np.sqrt(len(x)))
|
2629
|
-
df[retname1]=df["Daily Ret"].expanding(min_periods=
|
2698
|
+
df[retname1]=df["Daily Ret"].expanding(min_periods=min_periods).apply(lambda x: lpsd(x))
|
2630
2699
|
#df[retname1]=df["Daily Ret"].expanding(min_periods=5).apply(lambda x: lpsd(x))
|
2631
2700
|
df[retname2]=df[retname1]*100.0
|
2632
2701
|
|
@@ -2634,12 +2703,67 @@ def expanding_ret_lpsd(df0,basedate):
|
|
2634
2703
|
retname3="Exp Adj Ret LPSD"
|
2635
2704
|
retname4=retname3+'%'
|
2636
2705
|
#df[retname3]=df["Daily Adj Ret"].expanding(min_periods=1).apply(lambda x: lpsd(x)*np.sqrt(len(x)))
|
2637
|
-
df[retname3]=df["Daily Adj Ret"].expanding(min_periods=
|
2706
|
+
df[retname3]=df["Daily Adj Ret"].expanding(min_periods=min_periods).apply(lambda x: lpsd(x))
|
2638
2707
|
#df[retname3]=df["Daily Adj Ret"].expanding(min_periods=5).apply(lambda x: lpsd(x))
|
2639
2708
|
df[retname4]=df[retname3]*100.0
|
2640
2709
|
|
2641
2710
|
return df
|
2642
2711
|
|
2712
|
+
#==============================================================================
|
2713
|
+
def expanding_ret_lpsd(df0,basedate,min_periods=1):
|
2714
|
+
"""
|
2715
|
+
功能:基于日收益率数据集,从起始日期basedate开始的收益率损失风险扩展窗口序列。
|
2716
|
+
输入:
|
2717
|
+
日收益率数据集df。
|
2718
|
+
输出:扩展调整收益率波动风险序列,按照日期升序排列。
|
2719
|
+
|
2720
|
+
新算法:解决开始部分过度波动的诡异现象
|
2721
|
+
"""
|
2722
|
+
collist=list(df0)
|
2723
|
+
|
2724
|
+
if not ("Daily Ret" in collist):
|
2725
|
+
df0["Daily Ret"]=df0['Close'].pct_change()
|
2726
|
+
#df0["Daily Ret"]=df0["Daily Ret"].fillna(method='bfill', axis=1)
|
2727
|
+
df0["Daily Ret"]=df0["Daily Ret"].interpolate()
|
2728
|
+
|
2729
|
+
if not ("Daily Adj Ret" in collist):
|
2730
|
+
df0["Daily Adj Ret"]=df0['Adj Close'].pct_change()
|
2731
|
+
#df0["Daily Adj Ret"]=df0["Daily Adj Ret"].fillna(method='bfill', axis=1)
|
2732
|
+
df0["Daily Adj Ret"]=df0["Daily Adj Ret"].interpolate()
|
2733
|
+
|
2734
|
+
import pandas as pd
|
2735
|
+
basedate_pd=pd.to_datetime(basedate)
|
2736
|
+
df=df0[df0.index >= basedate_pd]
|
2737
|
+
|
2738
|
+
# 计算Exp Ret和Exp Adj Ret
|
2739
|
+
if not ('Exp Ret' in collist):
|
2740
|
+
df['Exp Ret'] = (1 + df['Daily Ret']).cumprod() - 1
|
2741
|
+
df['Exp Ret%'] = df['Exp Ret'] * 100.0
|
2742
|
+
|
2743
|
+
if not ('Exp Adj Ret' in collist):
|
2744
|
+
df['Exp Adj Ret'] = (1 + df['Daily Adj Ret']).cumprod() - 1
|
2745
|
+
df['Exp Adj Ret%'] = df['Exp Adj Ret'] * 100.0
|
2746
|
+
|
2747
|
+
#计算扩展窗口调整收益率下偏标准差:基于普通收益率
|
2748
|
+
retname1="Exp Ret LPSD"
|
2749
|
+
retname2=retname1+'%'
|
2750
|
+
import numpy as np
|
2751
|
+
#df[retname1]=df["Exp Ret"].expanding(min_periods=min_periods).apply(lambda x: lpsd(x))
|
2752
|
+
df[retname1]=df["Exp Ret"].expanding().apply(lambda x: lpsd(x))
|
2753
|
+
#df[retname1]=df["Exp Ret"].expanding().apply(downside_std, raw=True)
|
2754
|
+
df[retname2]=df[retname1]*100.0
|
2755
|
+
|
2756
|
+
#计算扩展窗口调整下偏标准差:基于调整收益率
|
2757
|
+
retname3="Exp Adj Ret LPSD"
|
2758
|
+
retname4=retname3+'%'
|
2759
|
+
#df[retname3]=df["Exp Adj Ret"].expanding(min_periods=min_periods).apply(lambda x: lpsd(x))
|
2760
|
+
df[retname3]=df["Exp Adj Ret"].expanding().apply(lambda x: lpsd(x))
|
2761
|
+
#df[retname3]=df["Exp Adj Ret"].expanding().apply(downside_std, raw=True)
|
2762
|
+
df[retname4]=df[retname3]*100.0
|
2763
|
+
|
2764
|
+
return df
|
2765
|
+
|
2766
|
+
|
2643
2767
|
if __name__ =="__main__":
|
2644
2768
|
basedate='2019-1-1'
|
2645
2769
|
pricedf=get_price('000002.SZ','2018-1-1','2020-3-16')
|
siat/security_trend2.py
CHANGED
@@ -575,7 +575,7 @@ def security_trend(ticker,indicator='Close',adjust='', \
|
|
575
575
|
attention_value=attention_value,attention_value_area=attention_value_area, \
|
576
576
|
attention_point=attention_point,attention_point_area=attention_point_area, \
|
577
577
|
band_area=band_area, \
|
578
|
-
graph=graph,axhline_value=0,axhline_label='', \
|
578
|
+
graph=graph,axhline_value=0,axhline_label='',power=power, \
|
579
579
|
loc1=loc1, \
|
580
580
|
printout=printout, \
|
581
581
|
sortby=sortby,trailing=trailing,trend_threshhold=trend_threshhold, \
|
@@ -599,7 +599,7 @@ def security_trend(ticker,indicator='Close',adjust='', \
|
|
599
599
|
attention_value=attention_value,attention_value_area=attention_value_area, \
|
600
600
|
attention_point=attention_point,attention_point_area=attention_point_area, \
|
601
601
|
band_area=band_area, \
|
602
|
-
graph=graph,facecolor=facecolor,loc=loc1, \
|
602
|
+
graph=graph,facecolor=facecolor,loc=loc1,power=power, \
|
603
603
|
annotate=annotate,annotate_value=annotate_value, \
|
604
604
|
mark_top=mark_top,mark_bottom=mark_bottom, \
|
605
605
|
mark_start=mark_start,mark_end=mark_end, \
|
siat/stock.py
CHANGED
@@ -566,13 +566,18 @@ def all_calculate(pricedf,ticker1,fromdate,todate,ticker_type='auto'):
|
|
566
566
|
|
567
567
|
# 横向拼接合并
|
568
568
|
result=pd.concat([df1a,df1b,df1c,df1d],axis=1,join='outer')
|
569
|
+
# 合并后产生的重复字段仅保留第一次出现的
|
570
|
+
result3 = result.loc[:, ~result.columns.duplicated(keep='first')]
|
571
|
+
|
569
572
|
|
570
573
|
# 去掉重复的列,但要避免仅仅因为数值相同而去掉有用的列,比如误删'Close'列
|
574
|
+
"""
|
571
575
|
result1=result.T
|
572
576
|
result1['item']=result1.index #在行中增加临时列名,避免误删
|
573
577
|
result2=result1.drop_duplicates(subset=None,keep='first',ignore_index=False)
|
574
578
|
result2.drop("item", axis=1, inplace=True) #去掉临时列名
|
575
579
|
result3=result2.T
|
580
|
+
"""
|
576
581
|
|
577
582
|
return result3
|
578
583
|
|
@@ -649,7 +654,12 @@ if __name__ =="__main__":
|
|
649
654
|
indicator='Close'
|
650
655
|
fromdate='2025-1-1'; todate='2025-6-15'
|
651
656
|
|
652
|
-
|
657
|
+
# 测试组9
|
658
|
+
ticker='JD'
|
659
|
+
indicator='Exp Ret%'
|
660
|
+
fromdate='2025-4-1'; todate='2025-6-30'
|
661
|
+
|
662
|
+
zeroline=False; adjust=''
|
653
663
|
attention_value='';attention_value_area=''
|
654
664
|
attention_point='';attention_point_area=''
|
655
665
|
average_value=False
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: siat
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.11.1
|
4
4
|
Summary: Securities Investment Analysis Tools (siat)
|
5
5
|
Home-page: https://pypi.org/project/siat/
|
6
6
|
Author: Prof. WANG Dehong, International Business School, Beijing Foreign Studies University
|
@@ -11,7 +11,7 @@ Project-URL: Homepage, https://pypi.org/project/siat/
|
|
11
11
|
Keywords: investment,finance,technical analysis,siat
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
13
13
|
Classifier: Operating System :: OS Independent
|
14
|
-
Requires-Python: <3.13,>=3.
|
14
|
+
Requires-Python: <3.13,>=3.7
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
License-File: LICENSE
|
17
17
|
Requires-Dist: pandas-datareader
|
@@ -48,6 +48,8 @@ Requires-Dist: tiingo[pandas]
|
|
48
48
|
Requires-Dist: numpy <2
|
49
49
|
Requires-Dist: playwright
|
50
50
|
Requires-Dist: lxml ==4.9.4
|
51
|
+
Requires-Dist: pymupdf
|
52
|
+
Requires-Dist: pypandoc
|
51
53
|
|
52
54
|
|
53
55
|
# What is siat?
|
@@ -1,5 +1,5 @@
|
|
1
1
|
siat/__init__.py,sha256=Y21NfAoDxQ3srK1tK-j8EQHzovAA4V_0ntqH8Sa_1E0,2236
|
2
|
-
siat/allin.py,sha256
|
2
|
+
siat/allin.py,sha256=afvsi6YSXnNUG1cjCqLVS3qfio-NuFNKlu-jVa3h0Rw,3157
|
3
3
|
siat/assets_liquidity.py,sha256=OnE_DyTznIs_m76MtszIvPXFVIjvB4_X2D3Y2-hlVO8,33892
|
4
4
|
siat/beta_adjustment.py,sha256=u_EZt3rEbvXDpqcJp_hUh9637P5vsrRHEfX6uG9Uin8,37292
|
5
5
|
siat/beta_adjustment_china.py,sha256=z17bstK2WtlKKqUl6aCcP3Pv661PWgyWqAqGHGUH7Yk,20807
|
@@ -9,8 +9,8 @@ siat/bond_base.py,sha256=ClHJ5dzjoO9knGhX65Sbyk0i0uKQpmdKGUblR-wrXTs,37681
|
|
9
9
|
siat/bond_china.py,sha256=WzUhjYYk8tsr3BDWLQcpuj9DqNxTzBSIi_wuAOZ48kY,3082
|
10
10
|
siat/bond_zh_sina.py,sha256=26BohGcS120utwqg9dJvdGm5OkuNpNu5bco80uOuQpU,4423
|
11
11
|
siat/capm_beta.py,sha256=t8-xr90II0JzbjsTOZNpRze_mKTvBRXjwN2o0N0tgD8,30521
|
12
|
-
siat/capm_beta2.py,sha256=
|
13
|
-
siat/common.py,sha256=
|
12
|
+
siat/capm_beta2.py,sha256=S2x6PrWp_1FyzVmG2MVzCf7LlpfHHEJxroJH2b26DvQ,35989
|
13
|
+
siat/common.py,sha256=GLNRbXP7uDA_pibWXJQ-St0o9ylhvRut0k9KpCQ70bI,193909
|
14
14
|
siat/compare_cross.py,sha256=3iP9TH2h3w27F2ARZc7FjKcErYCzWRc-TPiymOyoVtw,24171
|
15
15
|
siat/copyrights.py,sha256=YMLjZb328YpFMR-s_GUu0HBgeGce3pV7DgRut8S3I7w,690
|
16
16
|
siat/cryptocurrency.py,sha256=QSc4jK9VFlqBWVu-0th1BIMt8wC-5R5sWky3EaNupy0,27940
|
@@ -32,12 +32,12 @@ siat/fund_china.pickle,sha256=x_nPPdwy7wzIhtZQOplgDyTSyyUdXy9lbNxWysq7N6k,243777
|
|
32
32
|
siat/fund_china.py,sha256=U7bN8mOJ_4RBkxRzrR26LSj4YJyMNpRjBtrZNUH8JI4,138286
|
33
33
|
siat/future_china.py,sha256=LORFv7AaaQHq9QBk9ZSVVOjmxY_YWyPVRdpDxfCJvdo,17828
|
34
34
|
siat/google_authenticator.py,sha256=ZUbZR8OW0IAKDbcYtlqGqIpZdERpFor9NccFELxg9yI,1637
|
35
|
-
siat/grafix.py,sha256=
|
35
|
+
siat/grafix.py,sha256=ftU220aS5e_kWHH-VxkDGzgXBwEXrndlQBefT5TJ_iM,147167
|
36
36
|
siat/holding_risk.py,sha256=SCHVxRBEhseUrgMpsnvR9Pt6ns-V-voRl3hCuK1p5y4,31114
|
37
37
|
siat/luchy_draw.py,sha256=8Ue-NKnvSVqINPY1eXat0NJat5MR-gex_K62aOYFdmA,20486
|
38
38
|
siat/market_china.py,sha256=Ki9Kpq-fwA9F_uI_-0b2KS0ir1gkOwQfB5Yd_hCWSeg,51758
|
39
39
|
siat/markowitz.py,sha256=PtQ_6rLyh5SEXyO7SCDyYChcgXl6ddcdgQ06HETjDVE,97990
|
40
|
-
siat/markowitz2.py,sha256=
|
40
|
+
siat/markowitz2.py,sha256=XOQwwNRehVyjNO9ff9CIMq9mC2lDz1QZTWroVNBOGzQ,133241
|
41
41
|
siat/markowitz2_20250704.py,sha256=x10MfBaWZ42xcmDAbPU02oOZ4J02QDB1nyVKX8QobiA,126468
|
42
42
|
siat/markowitz2_20250705.py,sha256=jwDhQUvr5fcjA7scYbI8bJo-5zFPE4LyUsnK-hlqz90,133997
|
43
43
|
siat/markowitz_simple.py,sha256=aJVvx669ngcCsqoQtA9kvFOQVjsuipYt2fyTc4yMItE,14286
|
@@ -47,15 +47,17 @@ siat/option_china.py,sha256=16I9_e7OG0ziHtBgwjp9ss2GEwPZGoCWYd_3KFJ9V5E,123631
|
|
47
47
|
siat/option_pricing.py,sha256=gB5k-LQ3VOIdyllsW1xUtAT9Me2nTfl_kueysb1JmYE,74278
|
48
48
|
siat/other_indexes.py,sha256=68MDpQOBuiCOC4w0HMqNDihudMOkK7qnvgLbtpeHyt0,14084
|
49
49
|
siat/risk_adjusted_return.py,sha256=Q4ZRdTF57eNt4QCjeQ7uA8nG56Jls8f_QfJasZQEo0M,58748
|
50
|
-
siat/risk_adjusted_return2.py,sha256=
|
50
|
+
siat/risk_adjusted_return2.py,sha256=gCtHhfGNlV1wHqU9gfHJ_n17wRSyTMxc7lS8jgZ-GQk,87409
|
51
51
|
siat/risk_evaluation.py,sha256=xfgLSKlIWYmRJrIL4kn2k2hp9fyOMAzYGIhi9ImvKOw,88917
|
52
52
|
siat/risk_free_rate.py,sha256=IBuRqA2kppdZsW4D4fapW7vnM5HMEXOn95A5r9Pkwlo,12384
|
53
|
+
siat/save2docx.py,sha256=0JWjE_kQ8ea_NymzfqgVtcb11vrpzOWPsKPhIL9YaxU,12258
|
54
|
+
siat/save2pdf.py,sha256=o6FLEbGX1qjQtSmCuBGUTuzOCi_TH3f6yWGXc2L9knk,5075
|
53
55
|
siat/sector_china.py,sha256=uLsDXdRBDVfgG6tnXWnQOTyDmyZfglVO9DRUYU2e3pk,157914
|
54
56
|
siat/security_price2.py,sha256=DDiZ2dlv_TYPLhA8-gGb9i9xrl88r4rgSMEcxqQ6aU0,28065
|
55
|
-
siat/security_prices.py,sha256=
|
57
|
+
siat/security_prices.py,sha256=X3ip0q_m3OL3QRNRkr_lYQk-wsXLf6dWkFkyoZijhck,129368
|
56
58
|
siat/security_trend.py,sha256=o0vpWdrJkmODCP94X-Bvn-w7efHhj9HpUYBHtLl55D0,17240
|
57
|
-
siat/security_trend2.py,sha256=
|
58
|
-
siat/stock.py,sha256=
|
59
|
+
siat/security_trend2.py,sha256=Z8AvyYFtZsJcmjkRbAyV7i3suRK3IYlQr6eTB_K_q-4,31786
|
60
|
+
siat/stock.py,sha256=fJzj9zC0eCShsEbYG5H-fFBgmlbRVLwx09vn6uvrYso,161705
|
59
61
|
siat/stock_advice_linear.py,sha256=-twT7IGP-NEplkL1WPSACcNJjggRB2j4mlAQCkzOAuo,31655
|
60
62
|
siat/stock_base.py,sha256=uISvbRyOGy8p9QREA96CVydgflBkn5L3OXOGKl8oanc,1312
|
61
63
|
siat/stock_china.py,sha256=vHIc2UuXIGRkRvyL4fjTaNAoyFaq022p9FxPah6dscI,96399
|
@@ -71,8 +73,8 @@ siat/valuation.py,sha256=xGizcKJZ3ADLWWHm2TFQub18FxiDv2doQwBwbEqyqz0,51980
|
|
71
73
|
siat/valuation_china.py,sha256=eSKIDckyjG8QkENlW_OKkqbQHno8pzDcomBO9iGNJVM,83079
|
72
74
|
siat/var_model_validation.py,sha256=loqziBYO2p0xkeWm3Rb1rJsDhbcgAZ5aR9rBLRwLU5E,17624
|
73
75
|
siat/yf_name.py,sha256=laNKMTZ9hdenGX3IZ7G0a2RLBKEWtUQJFY9CWuk_fp8,24058
|
74
|
-
siat-3.
|
75
|
-
siat-3.
|
76
|
-
siat-3.
|
77
|
-
siat-3.
|
78
|
-
siat-3.
|
76
|
+
siat-3.11.1.dist-info/LICENSE,sha256=NTEMMROY9_4U1szoKC3N2BLHcDd_o5uTgqdVH8tbApw,1071
|
77
|
+
siat-3.11.1.dist-info/METADATA,sha256=hCgZFWX7IItZkrypnBYFGwCdBAZt6_DzmdlqWPge8UQ,8584
|
78
|
+
siat-3.11.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
79
|
+
siat-3.11.1.dist-info/top_level.txt,sha256=X5R8wrVviq8agwJFVRVDsufkuOJuit-1qAT_kXeptrY,17
|
80
|
+
siat-3.11.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|