siat 3.10.133__py3-none-any.whl → 3.11.2__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 CHANGED
@@ -135,3 +135,11 @@ from siat.luchy_draw import *
135
135
  # 搜索全球上市公司的英文名称,需要访问雅虎
136
136
  from siat.yf_name import *
137
137
 
138
+ # Save to PDF:实验中
139
+ from siat.save2pdf import *
140
+
141
+ # Save to docx:实验中
142
+ from siat.save2docx import *
143
+
144
+
145
+
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(jupyter2pdf2): playwright seems not fully installed yet")
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("File not found for {}".format(notebook_path))
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
- #不在必要,被attention_value的逻辑替代
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
@@ -21,6 +21,7 @@ from siat.common import *
21
21
  from siat.translate import *
22
22
  from siat.security_prices import *
23
23
  from siat.security_price2 import *
24
+ from siat.stock import *
24
25
  from siat.grafix import *
25
26
  #from siat.fama_french import *
26
27
 
@@ -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,347 @@
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
+ print(f"Converting to docx ...")
339
+
340
+ result = convert_ipynb_to_docx(ipynb_path, docx_path=None, page_size=page_size)
341
+ print(f"{result} created with TOC in {page_size} size")
342
+ print(f"However, TOC needs update manually in Microsoft Word")
343
+ print(f"And, title and some other things may need fine tuned as well")
344
+
345
+ return
346
+
347
+
siat/save2pdf.py ADDED
@@ -0,0 +1,147 @@
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
+ print(f"Converting to PDF ...")
45
+
46
+ # 1. 读 notebook → 提取目录结构
47
+ nb = nbformat.read(ipynb_path, as_version=4)
48
+ toc = _extract_toc(nb)
49
+
50
+ # 2. nb → HTML
51
+ exporter = HTMLExporter()
52
+ html_body, _ = exporter.from_notebook_node(nb)
53
+
54
+ # 3. 临时写 HTML / PDF
55
+ with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as th:
56
+ th.write(html_body)
57
+ html_path = th.name
58
+ with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tp:
59
+ tmp_pdf = tp.name
60
+
61
+ # 4. Playwright 渲染 HTML → PDF
62
+ asyncio.run(_html_to_pdf(html_path, tmp_pdf))
63
+
64
+ # 5. PyMuPDF 添加书签
65
+ _add_bookmarks(tmp_pdf, output_pdf, toc)
66
+
67
+ # 6. 清理
68
+ os.unlink(html_path)
69
+ os.unlink(tmp_pdf)
70
+
71
+ from pathlib import Path
72
+ full_path = Path(output_pdf)
73
+ # 提取文件名
74
+ filename = full_path.name # 'report.pdf'
75
+ # 提取路径
76
+ directory = full_path.parent # PosixPath('/Users/peter/Documents')
77
+
78
+ print(f"✅ {filename} is created with TOC")
79
+ print(f"✅ It is in {directory}")
80
+
81
+ #return output_pdf
82
+ return
83
+
84
+ async def _html_to_pdf(html_path: str, pdf_path: str):
85
+ async with async_playwright() as p:
86
+ browser = await p.chromium.launch()
87
+ page = await browser.new_page()
88
+ await page.goto(f"file://{html_path}")
89
+ await page.pdf(
90
+ path=pdf_path,
91
+ #format="A4",
92
+ format="A3",
93
+ print_background=True,
94
+ margin={"top":"20mm","bottom":"20mm","left":"20mm","right":"20mm"},
95
+ )
96
+ await browser.close()
97
+
98
+ def _extract_toc(nb_node) -> list[tuple[int,str]]:
99
+ """
100
+ 从每个 markdown 单元首行提取 # 级别和标题文本,
101
+ 返回 [(level, title), …]
102
+ """
103
+ toc = []
104
+ for cell in nb_node.cells:
105
+ if cell.cell_type != "markdown":
106
+ continue
107
+ first = cell.source.strip().splitlines()[0]
108
+ m = re.match(r"^(#{1,6})\s+(.*)", first)
109
+ if m:
110
+ toc.append((len(m.group(1)), m.group(2).strip()))
111
+ return toc
112
+
113
+ def _add_bookmarks(input_pdf: str, output_pdf: str, toc: list[tuple[int,str]]):
114
+ """
115
+ 用 PyMuPDF 打开临时 PDF,按 toc 列表查找页码,
116
+ 然后用 set_toc() 批量写入书签。
117
+ """
118
+ doc = fitz.open(input_pdf)
119
+ outline = []
120
+ for level, title in toc:
121
+ page_num = 1
122
+ # 搜索标题出现在第几页(0-based → +1)
123
+ for i in range(doc.page_count):
124
+ if title in doc.load_page(i).get_text():
125
+ page_num = i + 1
126
+ break
127
+ outline.append([level, title, page_num])
128
+
129
+ # 批量设置目录书签
130
+ doc.set_toc(outline)
131
+ doc.save(output_pdf)
132
+
133
+ # 使用示例(另起一个 cell 运行):
134
+ # ipynb = globals().get("__session__")
135
+ # ipynb2pdf(ipynb)
136
+
137
+
138
+ #==============================================================================
139
+
140
+ #==============================================================================
141
+ #==============================================================================
142
+ #==============================================================================
143
+ #==============================================================================
144
+ #==============================================================================
145
+ #==============================================================================
146
+ #==============================================================================
147
+ #==============================================================================
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 expanding_ret_volatility(df0,basedate):
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 expanding_ret_lpsd(df0,basedate):
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=1).apply(lambda x: lpsd(x))
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=1).apply(lambda x: lpsd(x))
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
- zeroline=False
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.10.133
3
+ Version: 3.11.2
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.12
14
+ Requires-Python: >=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=--32Bt2Mfg7l38w7X9cLJCdWtYRB3tTtVHnS9WnqKDI,3035
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=4g8pOFCwFrEpLx2NJbhL2nl_nrWaOwgbPCHx1G6P_tI,35949
13
- siat/common.py,sha256=PctlUcwb7MheTR-Ync-4hYWjPidsT9LufEsZXNJqbjw,193728
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=TvmbEAsdWKOJA5JK8xcwQz0vnoqq67AtGbRWbw-Vosk,145100
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=elpZr1nsALlrdxbMPT7pBREpA6I5P7TL6qpuLmMKPA8,133215
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=U1iKADTnWjywCkABPtr6tICYY8zmiLZyuENMLV0yMZk,87216
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=c43X3IGgfli4gg2VJilRaLzd_KCZX4yF_lLO2S9FvQA,12302
54
+ siat/save2pdf.py,sha256=cB1L5lH2n6RfgubCLFR7a617OGnrtT9IQhWXWGncoFs,5114
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=vbz85xjxMBFfipTcmF4ZnpOODnOKuEm4NfTKMi1C3OY,124171
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=czagiFIU3A3ow_dMn_-xQYnzgSTOP5Ds38PtHC1uyP0,31762
58
- siat/stock.py,sha256=ufhC3CWfx6KEDgCSTSwplf1EAi8AIlKmy6kdkoBAkbE,161426
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.10.133.dist-info/LICENSE,sha256=NTEMMROY9_4U1szoKC3N2BLHcDd_o5uTgqdVH8tbApw,1071
75
- siat-3.10.133.dist-info/METADATA,sha256=9y4HQxe8Jl1vjMqwkIDruTWGU6GMpHI49LDTHSeUW50,8538
76
- siat-3.10.133.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
77
- siat-3.10.133.dist-info/top_level.txt,sha256=X5R8wrVviq8agwJFVRVDsufkuOJuit-1qAT_kXeptrY,17
78
- siat-3.10.133.dist-info/RECORD,,
76
+ siat-3.11.2.dist-info/LICENSE,sha256=NTEMMROY9_4U1szoKC3N2BLHcDd_o5uTgqdVH8tbApw,1071
77
+ siat-3.11.2.dist-info/METADATA,sha256=Grx8mscqa3XjrFESmS2nlvTdGt_jFos7lvIeDo6lVXE,8578
78
+ siat-3.11.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
79
+ siat-3.11.2.dist-info/top_level.txt,sha256=X5R8wrVviq8agwJFVRVDsufkuOJuit-1qAT_kXeptrY,17
80
+ siat-3.11.2.dist-info/RECORD,,
File without changes