chore: import upstream snapshot with attribution
@@ -0,0 +1,12 @@
|
||||
# LLM Providers (set the one you use)
|
||||
OPENAI_API_KEY=
|
||||
SILICONFLOW_API_KEY=
|
||||
GOOGLE_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
XAI_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
|
||||
# X Brief LLM (orchestrator,默认 SiliconFlow MiniMax-M2.5)
|
||||
XBRIEF_ORCHESTRATOR_PROVIDER=siliconflow
|
||||
XBRIEF_ORCHESTRATOR_MODEL=Pro/MiniMaxAI/MiniMax-M2.5
|
||||
XBRIEF_ORCHESTRATOR_BASE_URL=https://api.siliconflow.cn/v1
|
||||
@@ -0,0 +1,239 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
# Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
# poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
# pdm.lock
|
||||
# pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
# pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# .idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
# Cache
|
||||
**/data_cache/
|
||||
|
||||
3月30日/*
|
||||
images/*
|
||||
|
||||
# Runtime outputs
|
||||
eval_results/
|
||||
reports/
|
||||
results/
|
||||
# Runtime data
|
||||
data/dashboard/history/
|
||||
data/dashboard/latest.json
|
||||
data/cache/
|
||||
data/portfolio.json
|
||||
|
||||
eval_results/*
|
||||
|
||||
docs/data_report.md
|
||||
docs/webui_prd.md
|
||||
docs/strategy_module_future.md
|
||||
docs/ui_shell.md
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,160 @@
|
||||
# 个性化多智能体金融分析工具
|
||||
|
||||
## 基于TradingAgents底层框架打造
|
||||
|
||||
- 【2026-04】搭建Webui仪表盘作为观察大盘数据的入口,个股分析作为TradingAgents分析工具的入口
|
||||
- 【2026-04】后续预告:交易策略工具搭建中,包括回测,开仓信号,平仓信号等
|
||||
|
||||
基于 TradingAgents 多智能体框架,提供:
|
||||
|
||||
- 交互式 CLI 分析流程
|
||||
- Streamlit WebUI(市场总览、个股分析、报告回看、持仓管理)
|
||||
- 本地报告与快照持久化能力
|
||||
|
||||
> 仅用于研究与学习,不构成任何投资建议。
|
||||
|
||||
---
|
||||
|
||||
## 当前功能(WebUI)
|
||||
|
||||
WebUI 入口:`app.py`
|
||||
|
||||
一级页面(当前实现):
|
||||
|
||||
1. **仪表盘**
|
||||
- 指数、宏观代理、板块 Top3、Top10 新闻
|
||||
- LLM 新闻立场与摘要
|
||||
- LLM 大盘总结
|
||||
- 手动刷新并写入快照(latest + history)
|
||||
2. **个股分析**
|
||||
- 启动分析任务、查看运行进度与日志
|
||||
- 展示最新报告摘要
|
||||
- 内嵌历史报告浏览
|
||||
3. **策略**
|
||||
- 当前为占位页(后续实现)
|
||||
4. **股票筛选**
|
||||
- 搜索/排序/查看 ticker
|
||||
- 跳转股票详情页
|
||||
5. **股票详情**
|
||||
- 概览指标、最新分析引导、历史报告
|
||||
6. **持仓**
|
||||
- 本地持仓录入、同 ticker 合并、均价重算
|
||||
|
||||
产品规格文档见:`docs/webui_prd.md`
|
||||
|
||||
---
|
||||
|
||||
## 项目结构(核心目录)
|
||||
|
||||
```text
|
||||
TradingAgents/
|
||||
├── app.py # WebUI 入口
|
||||
├── web/
|
||||
│ ├── pages/ # 各页面实现
|
||||
│ ├── services/ # 数据拉取/存储/LLM 服务
|
||||
│ ├── theme.py # 全局主题样式
|
||||
│ ├── sidebar_nav.py # 侧栏导航
|
||||
│ ├── history.py # 报告历史扫描
|
||||
│ └── report_viewer.py # 报告渲染
|
||||
├── tradingagents/ # 多智能体核心框架
|
||||
├── cli/ # CLI 入口与交互
|
||||
├── docs/ # 文档
|
||||
├── data/ # 本地运行数据(建议忽略运行产物)
|
||||
├── reports/ # Web 分析报告输出(运行产物)
|
||||
├── results/ # CLI 结果输出(运行产物)
|
||||
└── eval_results/ # 评估日志输出(运行产物)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/qchen34/TradingAgents.git
|
||||
cd TradingAgents
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 环境变量
|
||||
|
||||
至少配置一个 LLM Provider 的 Key(按需):
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=...
|
||||
export SILICONFLOW_API_KEY=...
|
||||
export GOOGLE_API_KEY=...
|
||||
export ANTHROPIC_API_KEY=...
|
||||
export XAI_API_KEY=...
|
||||
export OPENROUTER_API_KEY=...
|
||||
export ALPHA_VANTAGE_API_KEY=...
|
||||
```
|
||||
|
||||
也可使用 `.env`:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 启动方式
|
||||
|
||||
### 1) 启动 WebUI
|
||||
|
||||
```bash
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
### 2) 启动 npm 前端 + Python API
|
||||
|
||||
```bash
|
||||
# API
|
||||
python -m uvicorn backend.api.main:app --host 127.0.0.1 --port 8000 --reload
|
||||
|
||||
# Frontend(新终端)
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3) 一键并行启动(Streamlit + FastAPI + Next.js)
|
||||
|
||||
```bash
|
||||
bash scripts/start_dual_stack.sh
|
||||
```
|
||||
|
||||
### 或
|
||||
### 4) 启动 CLI
|
||||
|
||||
```bash
|
||||
python -m cli.main
|
||||
# 或(安装后)
|
||||
tradingagents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 运行数据与输出目录说明
|
||||
|
||||
以下目录/文件通常为运行自动生成,建议加入 `.gitignore`:
|
||||
|
||||
- `eval_results/`
|
||||
- `reports/`
|
||||
- `results/`
|
||||
- `data/dashboard/history/`
|
||||
- `data/dashboard/latest.json`
|
||||
- `data/cache/`
|
||||
- `data/portfolio.json`
|
||||
|
||||
如果你希望仓库“干净可复现”,建议只保留代码与文档,运行产物不入库。
|
||||
|
||||
|
||||
|
||||
|
||||
## 引用
|
||||
|
||||
本项目底层引用Tauric Research的TradingAgents框架:https://github.com/TauricResearch/TradingAgents
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`qchen34/TradingAgents`
|
||||
- 原始仓库:https://github.com/qchen34/TradingAgents
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,43 @@
|
||||
import streamlit as st
|
||||
|
||||
from web.navigation import PAGE_DASHBOARD
|
||||
from web.pages import PAGES
|
||||
from web.sidebar_nav import render_sidebar_navigation
|
||||
from web.theme import apply_theme
|
||||
|
||||
|
||||
def _init_session_state() -> None:
|
||||
defaults = {
|
||||
"current_page": PAGE_DASHBOARD,
|
||||
"ui_mode": "idle",
|
||||
"show_config": False,
|
||||
"event_log": [],
|
||||
"runtime_stage": "",
|
||||
"latest_run_dir": "",
|
||||
"last_params": None,
|
||||
"selected_params_summary": "",
|
||||
"active_output_dir": "",
|
||||
"runtime_stats": {},
|
||||
"selected_ticker": "QQQ",
|
||||
"error": "",
|
||||
}
|
||||
for key, val in defaults.items():
|
||||
if key not in st.session_state:
|
||||
st.session_state[key] = val
|
||||
|
||||
|
||||
st.set_page_config(page_title="TradingAgents 控制台", layout="wide")
|
||||
_init_session_state()
|
||||
apply_theme()
|
||||
|
||||
page_names = list(PAGES.keys())
|
||||
if st.session_state.current_page not in page_names:
|
||||
st.session_state.current_page = page_names[0]
|
||||
|
||||
with st.sidebar:
|
||||
st.session_state.current_page = render_sidebar_navigation(
|
||||
page_names,
|
||||
st.session_state.current_page,
|
||||
)
|
||||
|
||||
PAGES[st.session_state.current_page]()
|
||||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 774 KiB |
|
After Width: | Height: | Size: 791 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 357 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 216 KiB |
@@ -0,0 +1,2 @@
|
||||
"""Backend services for API and future workers."""
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""FastAPI entry package for TradingAgents."""
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from backend.api.schemas import (
|
||||
ApiEnvelope,
|
||||
BatchQuoteRequest,
|
||||
BacktestJobRequest,
|
||||
DashboardRefreshRequest,
|
||||
LrsChatRequest,
|
||||
WheelEvaluateRequest,
|
||||
XBriefRefreshRequest,
|
||||
)
|
||||
from backend.api.services import (
|
||||
DEFAULT_SESSION_STATE,
|
||||
answer_lrs_chat,
|
||||
batch_quotes,
|
||||
create_backtest_job,
|
||||
get_dashboard_snapshot,
|
||||
get_backtest_job,
|
||||
refresh_dashboard_snapshot,
|
||||
refresh_x_brief,
|
||||
strategy_metadata,
|
||||
get_x_brief_latest,
|
||||
wheel_evaluate,
|
||||
wheel_profiles,
|
||||
)
|
||||
from web.services.backtesting_core import _PERIOD_ORDER, _PERIOD_SPECS
|
||||
|
||||
app = FastAPI(title="TradingAgents API", version="0.1.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
def _trace_id(req: Request) -> str:
|
||||
return req.headers.get("x-trace-id") or uuid.uuid4().hex
|
||||
|
||||
|
||||
def _ok(request: Request, data: dict[str, Any] | list[Any] | None = None) -> JSONResponse:
|
||||
payload = ApiEnvelope(success=True, data=data, trace_id=_trace_id(request))
|
||||
return JSONResponse(status_code=200, content=payload.model_dump())
|
||||
|
||||
|
||||
def _err(request: Request, msg: str, code: int = 500) -> JSONResponse:
|
||||
payload = ApiEnvelope(success=False, error=msg, data=None, trace_id=_trace_id(request))
|
||||
return JSONResponse(status_code=code, content=payload.model_dump())
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health(request: Request) -> JSONResponse:
|
||||
return _ok(request, {"status": "ok"})
|
||||
|
||||
|
||||
@app.get("/api/v1/state/defaults")
|
||||
def state_defaults(request: Request) -> JSONResponse:
|
||||
return _ok(request, DEFAULT_SESSION_STATE)
|
||||
|
||||
|
||||
@app.get("/api/v1/strategy/metadata")
|
||||
def get_strategy_metadata(request: Request) -> JSONResponse:
|
||||
return _ok(request, strategy_metadata())
|
||||
|
||||
|
||||
@app.get("/api/v1/dashboard")
|
||||
def get_dashboard(request: Request) -> JSONResponse:
|
||||
try:
|
||||
return _ok(request, get_dashboard_snapshot())
|
||||
except Exception as exc:
|
||||
return _err(request, f"仪表盘数据读取失败:{exc}", 500)
|
||||
|
||||
|
||||
@app.post("/api/v1/dashboard/refresh")
|
||||
def post_dashboard_refresh(body: DashboardRefreshRequest, request: Request) -> JSONResponse:
|
||||
try:
|
||||
return _ok(request, refresh_dashboard_snapshot(limit=body.limit))
|
||||
except Exception as exc:
|
||||
return _err(request, f"仪表盘刷新失败:{exc}", 500)
|
||||
|
||||
|
||||
@app.post("/api/v1/quotes/batch")
|
||||
def post_batch_quotes(body: BatchQuoteRequest, request: Request) -> JSONResponse:
|
||||
try:
|
||||
return _ok(request, batch_quotes(body.codes))
|
||||
except Exception as exc:
|
||||
return _err(request, f"批量报价失败:{exc}", 500)
|
||||
|
||||
|
||||
@app.get("/api/v1/x-brief/latest")
|
||||
def get_xbrief_latest(request: Request) -> JSONResponse:
|
||||
data = get_x_brief_latest()
|
||||
if data is None:
|
||||
return _ok(request, {"ready": False})
|
||||
return _ok(request, {"ready": True, **data})
|
||||
|
||||
|
||||
@app.post("/api/v1/x-brief/refresh")
|
||||
def post_xbrief_refresh(body: XBriefRefreshRequest, request: Request) -> JSONResponse:
|
||||
try:
|
||||
data = refresh_x_brief(days=body.days, per_account_limit=body.per_account_limit)
|
||||
return _ok(request, {"ready": True, **data})
|
||||
except Exception as exc:
|
||||
return _err(request, f"X资讯简报刷新失败:{exc}", 500)
|
||||
|
||||
|
||||
@app.post("/api/v1/strategy/lrs/chat")
|
||||
def post_lrs_chat(body: LrsChatRequest, request: Request) -> JSONResponse:
|
||||
try:
|
||||
answer = answer_lrs_chat(body.question, body.last_params)
|
||||
return _ok(request, {"answer": answer})
|
||||
except Exception as exc:
|
||||
return _err(request, f"LLM 调用失败:{exc}", 500)
|
||||
|
||||
|
||||
@app.get("/api/v1/wheel/profiles")
|
||||
def get_wheel_profiles(request: Request) -> JSONResponse:
|
||||
return _ok(request, wheel_profiles())
|
||||
|
||||
|
||||
@app.post("/api/v1/wheel/evaluate")
|
||||
def post_wheel_evaluate(body: WheelEvaluateRequest, request: Request) -> JSONResponse:
|
||||
try:
|
||||
data = wheel_evaluate(
|
||||
style_key=body.style_key,
|
||||
underlying_ticker=body.underlying_ticker,
|
||||
signal_ticker=body.signal_ticker,
|
||||
)
|
||||
return _ok(request, data)
|
||||
except Exception as exc:
|
||||
return _err(request, f"Wheel 评估失败:{exc}", 500)
|
||||
|
||||
|
||||
@app.get("/api/v1/backtest/periods")
|
||||
def get_backtest_periods(request: Request) -> JSONResponse:
|
||||
periods = [
|
||||
{"key": key, "label": _PERIOD_SPECS[key]["label"]}
|
||||
for key in _PERIOD_ORDER
|
||||
]
|
||||
return _ok(request, periods)
|
||||
|
||||
|
||||
@app.post("/api/v1/backtest/jobs")
|
||||
def post_backtest_job(body: BacktestJobRequest, request: Request) -> JSONResponse:
|
||||
job = create_backtest_job(body.period_key)
|
||||
return _ok(
|
||||
request,
|
||||
{
|
||||
"job_id": job.job_id,
|
||||
"status": job.status,
|
||||
"period_key": job.period_key,
|
||||
"created_at": job.created_at,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v1/backtest/jobs/{job_id}")
|
||||
def get_job(job_id: str, request: Request) -> JSONResponse:
|
||||
job = get_backtest_job(job_id)
|
||||
if job is None:
|
||||
return _err(request, "任务不存在", 404)
|
||||
return _ok(
|
||||
request,
|
||||
{
|
||||
"job_id": job.job_id,
|
||||
"status": job.status,
|
||||
"period_key": job.period_key,
|
||||
"created_at": job.created_at,
|
||||
"updated_at": job.updated_at,
|
||||
"result": job.result,
|
||||
"error": job.error,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ApiEnvelope(BaseModel):
|
||||
success: bool = True
|
||||
data: dict[str, Any] | list[Any] | None = None
|
||||
error: str | None = None
|
||||
trace_id: str
|
||||
|
||||
|
||||
class LrsChatRequest(BaseModel):
|
||||
question: str = Field(min_length=1)
|
||||
last_params: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class BacktestJobRequest(BaseModel):
|
||||
period_key: Literal["2y", "3y", "5y", "10y", "2015_2020", "2010_2015"]
|
||||
|
||||
|
||||
class DashboardRefreshRequest(BaseModel):
|
||||
limit: int = Field(default=10, ge=1, le=30)
|
||||
|
||||
|
||||
class WheelEvaluateRequest(BaseModel):
|
||||
style_key: Literal["aggressive", "neutral", "conservative"] = "neutral"
|
||||
underlying_ticker: str = Field(default="TQQQ", min_length=1)
|
||||
signal_ticker: str = Field(default="QQQ", min_length=1)
|
||||
|
||||
|
||||
class BatchQuoteRequest(BaseModel):
|
||||
codes: list[str] = Field(min_length=1, max_length=200)
|
||||
|
||||
|
||||
class XBriefRefreshRequest(BaseModel):
|
||||
days: int = Field(default=7, ge=3, le=30)
|
||||
per_account_limit: int = Field(default=80, ge=10, le=200)
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import yfinance as yf
|
||||
|
||||
from web.services import backtesting_core as bt
|
||||
from web.services import market_data as md
|
||||
from web.services.x_brief_data import collect_x_rss_last_week, normalize_to_x_url, save_raw_payload
|
||||
from web.services.x_brief_llm import build_fallback_modules, build_x_brief_modules, translate_display_content
|
||||
from web.services.x_brief_renderer import (
|
||||
build_monthly_analysis_markdown,
|
||||
build_tweet_summary_markdown,
|
||||
load_latest_view,
|
||||
persist_outputs,
|
||||
)
|
||||
from web.pages.wheel.style_profiles import STYLE_PROFILES
|
||||
from web.pages.wheel.strategy_shared import rsi
|
||||
from web.services.dashboard_llm import build_llm_config
|
||||
from web.services.strategy_api_service import answer_lrs_question, get_lrs_doc_markdown
|
||||
|
||||
|
||||
DEFAULT_SESSION_STATE: dict[str, Any] = {
|
||||
"current_page": "仪表盘",
|
||||
"ui_mode": "idle",
|
||||
"show_config": False,
|
||||
"event_log": [],
|
||||
"runtime_stage": "",
|
||||
"latest_run_dir": "",
|
||||
"last_params": None,
|
||||
"selected_params_summary": "",
|
||||
"active_output_dir": "",
|
||||
"runtime_stats": {},
|
||||
"selected_ticker": "QQQ",
|
||||
"error": "",
|
||||
"strategy_sub_menu": "LRS TQQQ策略",
|
||||
}
|
||||
|
||||
|
||||
def strategy_metadata() -> dict[str, Any]:
|
||||
return {
|
||||
"sub_strategies": ["LRS TQQQ策略", "Wheel策略"],
|
||||
"lrs_doc_md": get_lrs_doc_markdown(),
|
||||
}
|
||||
|
||||
|
||||
def answer_lrs_chat(question: str, last_params: dict[str, Any] | None) -> str:
|
||||
cfg = build_llm_config(last_params)
|
||||
prompt = (
|
||||
"你是量化策略研究助手。请围绕 LRS TQQQ 策略回答问题,"
|
||||
"要求中文、结构化、可执行。\n\n用户问题:"
|
||||
f"{question}"
|
||||
)
|
||||
return answer_lrs_question(prompt, cfg)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BacktestJob:
|
||||
job_id: str
|
||||
period_key: str
|
||||
status: str
|
||||
created_at: str
|
||||
result: dict[str, Any] | None = None
|
||||
error: str | None = None
|
||||
updated_at: str = field(default_factory=lambda: _now_iso())
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
_JOBS: dict[str, BacktestJob] = {}
|
||||
_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _run_backtest(period_key: str, job_id: str) -> None:
|
||||
try:
|
||||
qqq_df, tqqq_df = bt._fetch(period_key)
|
||||
if qqq_df.empty or tqqq_df.empty:
|
||||
raise RuntimeError("QQQ 或 TQQQ 数据为空")
|
||||
qqq_full = qqq_df["Close"].squeeze().dropna()
|
||||
tqqq_full = tqqq_df["Close"].squeeze().dropna()
|
||||
if len(qqq_full) < 220:
|
||||
raise RuntimeError(f"数据量不足({len(qqq_full)} 天),无法计算 MA200")
|
||||
trade_start = bt._get_effective_start(qqq_full, tqqq_full, period_key)
|
||||
qqq_eff = qqq_full[trade_start:]
|
||||
sig = bt._build_signals(qqq_full)
|
||||
trades, equity = bt._simulate(sig, tqqq_full, trade_start=trade_start)
|
||||
metrics = bt._metrics(trades, equity, qqq_eff)
|
||||
with _LOCK:
|
||||
_JOBS[job_id].status = "completed"
|
||||
_JOBS[job_id].updated_at = _now_iso()
|
||||
_JOBS[job_id].result = {
|
||||
"period_key": period_key,
|
||||
"period_label": bt._PERIOD_SPECS[period_key]["label"],
|
||||
"date_range": {
|
||||
"start": str(qqq_eff.index[0].date()),
|
||||
"end": str(qqq_eff.index[-1].date()),
|
||||
},
|
||||
"metrics": metrics,
|
||||
"trades": [t.__dict__ for t in trades],
|
||||
}
|
||||
except Exception as exc: # pragma: no cover - best effort background task
|
||||
with _LOCK:
|
||||
_JOBS[job_id].status = "failed"
|
||||
_JOBS[job_id].updated_at = _now_iso()
|
||||
_JOBS[job_id].error = str(exc)
|
||||
|
||||
|
||||
def create_backtest_job(period_key: str) -> BacktestJob:
|
||||
job_id = uuid.uuid4().hex
|
||||
job = BacktestJob(
|
||||
job_id=job_id,
|
||||
period_key=period_key,
|
||||
status="running",
|
||||
created_at=_now_iso(),
|
||||
)
|
||||
with _LOCK:
|
||||
_JOBS[job_id] = job
|
||||
th = threading.Thread(target=_run_backtest, args=(period_key, job_id), daemon=True)
|
||||
th.start()
|
||||
return job
|
||||
|
||||
|
||||
def get_backtest_job(job_id: str) -> BacktestJob | None:
|
||||
with _LOCK:
|
||||
return _JOBS.get(job_id)
|
||||
|
||||
|
||||
def _serialize_quote_row(row: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"ticker": getattr(row, "ticker", ""),
|
||||
"label": getattr(row, "label", ""),
|
||||
"price": getattr(row, "price", None),
|
||||
"prev_close": getattr(row, "prev_close", None),
|
||||
"change": getattr(row, "change", None),
|
||||
"change_pct": getattr(row, "change_pct", None),
|
||||
}
|
||||
|
||||
|
||||
def _serialize_snapshot(snapshot: dict[str, Any], news: list[dict[str, Any]] | None) -> dict[str, Any]:
|
||||
top6_raw = snapshot.get("top6_sectors")
|
||||
if top6_raw is None:
|
||||
top6_raw = snapshot.get("top3_sectors", [])
|
||||
return {
|
||||
"market_status": snapshot.get("market_status", "N/A"),
|
||||
"last_updated_et": snapshot.get("last_updated_et", "N/A"),
|
||||
"fetched_at_utc": snapshot.get("fetched_at_utc", ""),
|
||||
"llm_model": snapshot.get("llm_model", ""),
|
||||
"llm_provider": snapshot.get("llm_provider", ""),
|
||||
"llm_news_error": snapshot.get("llm_news_error", ""),
|
||||
"llm_digest_error": snapshot.get("llm_digest_error", ""),
|
||||
"market_digest_md": snapshot.get("market_digest_md", ""),
|
||||
"indexes": [_serialize_quote_row(x) for x in snapshot.get("indexes", [])],
|
||||
"top6_sectors": [_serialize_quote_row(x) for x in top6_raw],
|
||||
"macro_strip": snapshot.get("macro_strip") or [],
|
||||
"news": news or [],
|
||||
}
|
||||
|
||||
|
||||
def get_dashboard_snapshot() -> dict[str, Any]:
|
||||
snapshot, news = md.load_dashboard_display()
|
||||
if snapshot is None:
|
||||
snapshot = md.get_dashboard_market_snapshot()
|
||||
return _serialize_snapshot(snapshot, news)
|
||||
|
||||
|
||||
def refresh_dashboard_snapshot(limit: int = 10) -> dict[str, Any]:
|
||||
snapshot, news = md.refresh_dashboard_cache(limit=limit)
|
||||
return _serialize_snapshot(snapshot, news)
|
||||
|
||||
|
||||
def wheel_profiles() -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
for key, p in STYLE_PROFILES.items():
|
||||
out.append(
|
||||
{
|
||||
"key": key,
|
||||
"label": p.label,
|
||||
"dte": p.dte,
|
||||
"ivr_min": p.ivr_min,
|
||||
"rsi_max": p.rsi_max,
|
||||
"put_otm": p.put_otm,
|
||||
"call_otm": p.call_otm,
|
||||
"put_tp": p.put_tp,
|
||||
"call_tp": p.call_tp,
|
||||
"put_sl": p.put_sl,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_close(ticker: str) -> Any:
|
||||
df = yf.download(ticker, period="6mo", auto_adjust=True, progress=False)
|
||||
if df.empty:
|
||||
raise RuntimeError(f"{ticker} 无可用行情")
|
||||
close = df["Close"]
|
||||
if hasattr(close, "columns"):
|
||||
close = close.iloc[:, 0]
|
||||
return close.dropna()
|
||||
|
||||
|
||||
def _estimate_ivr(underlying_close: Any) -> float:
|
||||
ret = underlying_close.pct_change().dropna()
|
||||
if len(ret) < 20:
|
||||
return 50.0
|
||||
hv = ret.rolling(20).std() * np.sqrt(252)
|
||||
iv = (hv * 1.15).clip(lower=0.1, upper=2.5)
|
||||
iv_min = iv.rolling(126, min_periods=20).min()
|
||||
iv_max = iv.rolling(126, min_periods=20).max()
|
||||
ivr = ((iv - iv_min) / (iv_max - iv_min).replace(0, np.nan) * 100.0).fillna(50.0)
|
||||
return float(ivr.iloc[-1])
|
||||
|
||||
|
||||
def wheel_evaluate(style_key: str, underlying_ticker: str, signal_ticker: str) -> dict[str, Any]:
|
||||
if style_key not in STYLE_PROFILES:
|
||||
raise RuntimeError(f"未知风格:{style_key}")
|
||||
style = STYLE_PROFILES[style_key]
|
||||
ul = _fetch_close(underlying_ticker.upper())
|
||||
sig = _fetch_close(signal_ticker.upper())
|
||||
rs = rsi(sig, 14).fillna(50.0)
|
||||
latest_ul = float(ul.iloc[-1])
|
||||
latest_sig = float(sig.iloc[-1])
|
||||
latest_rsi = float(rs.iloc[-1])
|
||||
latest_ivr = _estimate_ivr(ul)
|
||||
|
||||
put_strike = round(latest_ul * (1.0 - style.put_otm), 2)
|
||||
call_strike = round(latest_ul * (1.0 + style.call_otm), 2)
|
||||
put_entry_ok = latest_ivr >= style.ivr_min and latest_rsi <= style.rsi_max
|
||||
regime = "bullish" if latest_sig >= float(sig.iloc[-20:].mean()) else "neutral_to_weak"
|
||||
if put_entry_ok:
|
||||
action = f"可考虑卖出现金担保Put(行权价约 {put_strike})"
|
||||
elif regime == "bullish":
|
||||
action = "等待更优波动率窗口(IVR 偏低),暂不卖Put"
|
||||
else:
|
||||
action = "信号偏弱,优先观望或缩小仓位"
|
||||
|
||||
return {
|
||||
"style": {
|
||||
"key": style.key,
|
||||
"label": style.label,
|
||||
"dte": style.dte,
|
||||
"ivr_min": style.ivr_min,
|
||||
"rsi_max": style.rsi_max,
|
||||
"put_otm": style.put_otm,
|
||||
"call_otm": style.call_otm,
|
||||
"put_tp": style.put_tp,
|
||||
"call_tp": style.call_tp,
|
||||
"put_sl": style.put_sl,
|
||||
},
|
||||
"signal": {
|
||||
"underlying_ticker": underlying_ticker.upper(),
|
||||
"signal_ticker": signal_ticker.upper(),
|
||||
"underlying_price": latest_ul,
|
||||
"signal_price": latest_sig,
|
||||
"rsi14": latest_rsi,
|
||||
"ivr_est": latest_ivr,
|
||||
"put_entry_ok": put_entry_ok,
|
||||
"regime": regime,
|
||||
},
|
||||
"execution_hint": {
|
||||
"suggested_action": action,
|
||||
"put_strike_hint": put_strike,
|
||||
"call_strike_hint": call_strike,
|
||||
"dte_hint": style.dte,
|
||||
"take_profit": {
|
||||
"put": style.put_tp,
|
||||
"call": style.call_tp,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_quote_code(code: str) -> str:
|
||||
c = (code or "").strip().upper()
|
||||
if not c:
|
||||
return ""
|
||||
if c.startswith("US.."):
|
||||
return f"^{c[4:]}"
|
||||
if c.startswith("US."):
|
||||
return c[3:]
|
||||
return c
|
||||
|
||||
|
||||
def batch_quotes(codes: list[str]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
for raw in codes:
|
||||
normalized = _normalize_quote_code(raw)
|
||||
if not normalized:
|
||||
continue
|
||||
q = md._quote(normalized, normalized)
|
||||
out.append(
|
||||
{
|
||||
"request_code": raw,
|
||||
"normalized_code": normalized,
|
||||
"ticker": q.ticker,
|
||||
"label": q.label,
|
||||
"price": q.price,
|
||||
"prev_close": q.prev_close,
|
||||
"change": q.change,
|
||||
"change_pct": q.change_pct,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def get_x_brief_latest() -> dict[str, Any] | None:
|
||||
return load_latest_view()
|
||||
|
||||
|
||||
def refresh_x_brief(days: int = 7, per_account_limit: int = 80) -> dict[str, Any]:
|
||||
payload = collect_x_rss_last_week(days=days, per_account_limit=per_account_limit)
|
||||
cfg = build_llm_config(None)
|
||||
# X Brief 两层模型:
|
||||
# - 第一层编排:thinking(MiniMax)
|
||||
# - 第二层翻译:quick(沿用 quick_think_llm)
|
||||
cfg["xbrief_orchestrator_provider"] = os.environ.get("XBRIEF_ORCHESTRATOR_PROVIDER", "siliconflow")
|
||||
cfg["xbrief_orchestrator_model"] = os.environ.get("XBRIEF_ORCHESTRATOR_MODEL", "Pro/MiniMaxAI/MiniMax-M2.5")
|
||||
cfg["xbrief_orchestrator_base_url"] = os.environ.get("XBRIEF_ORCHESTRATOR_BASE_URL", "https://api.siliconflow.cn/v1")
|
||||
cfg["xbrief_translate_provider"] = cfg.get("llm_provider")
|
||||
cfg["xbrief_translate_model"] = cfg.get("quick_think_llm")
|
||||
cfg["xbrief_translate_base_url"] = cfg.get("backend_url")
|
||||
save_raw_payload(payload)
|
||||
tweet_md = build_tweet_summary_markdown(payload)
|
||||
modules: dict[str, Any]
|
||||
try:
|
||||
modules = build_x_brief_modules(payload, tweet_md, cfg)
|
||||
except Exception as exc:
|
||||
modules = build_fallback_modules(payload)
|
||||
modules["risk_signals"] = modules.get("risk_signals", []) + [
|
||||
{
|
||||
"id": "llm-fallback-error",
|
||||
"level": "MID",
|
||||
"title": "LLM 编排暂不可用",
|
||||
"detail": f"本次使用规则兜底:{exc}",
|
||||
}
|
||||
]
|
||||
if not any(modules.get(k) for k in ("themes", "p0_events", "top_quotes", "category_updates", "risk_signals")):
|
||||
modules = build_fallback_modules(payload)
|
||||
|
||||
# 最终落盘前统一做一遍 URL 规范化,避免历史链路残留 nitter 链接。
|
||||
tweets = payload.get("tweets", []) or []
|
||||
norm_tweets: list[dict[str, Any]] = []
|
||||
for t in tweets:
|
||||
d = dict(t)
|
||||
d["url"] = normalize_to_x_url(str(d.get("url", "")), str(d.get("handle", "")))
|
||||
norm_tweets.append(d)
|
||||
payload["tweets"] = norm_tweets
|
||||
|
||||
for theme in modules.get("themes", []) or []:
|
||||
for sample in theme.get("samples", []) or []:
|
||||
sample["url"] = normalize_to_x_url(str(sample.get("url", "")), str(sample.get("handle", "")))
|
||||
for e in modules.get("p0_events", []) or []:
|
||||
e["url"] = normalize_to_x_url(str(e.get("url", "")))
|
||||
for q in modules.get("top_quotes", []) or []:
|
||||
speaker = str(q.get("speaker", "")).lstrip("@")
|
||||
q["url"] = normalize_to_x_url(str(q.get("url", "")), speaker)
|
||||
for c in modules.get("category_updates", []) or []:
|
||||
for item in c.get("items", []) or []:
|
||||
item["url"] = normalize_to_x_url(str(item.get("url", "")))
|
||||
|
||||
try:
|
||||
payload = translate_display_content(payload, modules, cfg)
|
||||
except Exception as exc:
|
||||
modules["risk_signals"] = (modules.get("risk_signals") or []) + [
|
||||
{
|
||||
"id": "llm-translate-fallback-error",
|
||||
"level": "MID",
|
||||
"title": "前端内容翻译层异常",
|
||||
"detail": f"本次展示内容使用原文兜底:{exc}",
|
||||
}
|
||||
]
|
||||
analysis_md = build_monthly_analysis_markdown(payload)
|
||||
|
||||
# 统计 modules 实际引用的唯一推文数,写入 overview
|
||||
_kept_urls: set[str] = set()
|
||||
for _theme in modules.get("themes", []) or []:
|
||||
for _s in _theme.get("samples", []) or []:
|
||||
if _s.get("url"):
|
||||
_kept_urls.add(_s["url"])
|
||||
for _e in modules.get("p0_events", []) or []:
|
||||
if _e.get("url"):
|
||||
_kept_urls.add(_e["url"])
|
||||
for _q in modules.get("top_quotes", []) or []:
|
||||
if _q.get("url"):
|
||||
_kept_urls.add(_q["url"])
|
||||
overview = dict(payload.get("overview") or {})
|
||||
overview["tweets_kept"] = len(_kept_urls)
|
||||
payload = dict(payload)
|
||||
payload["overview"] = overview
|
||||
|
||||
return persist_outputs(payload, tweet_md, analysis_md, modules=modules)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import getpass
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from cli.config import CLI_CONFIG
|
||||
|
||||
|
||||
def fetch_announcements(url: str = None, timeout: float = None) -> dict:
|
||||
"""Fetch announcements from endpoint. Returns dict with announcements and settings."""
|
||||
endpoint = url or CLI_CONFIG["announcements_url"]
|
||||
timeout = timeout or CLI_CONFIG["announcements_timeout"]
|
||||
fallback = CLI_CONFIG["announcements_fallback"]
|
||||
|
||||
try:
|
||||
response = requests.get(endpoint, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return {
|
||||
"announcements": data.get("announcements", [fallback]),
|
||||
"require_attention": data.get("require_attention", False),
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
"announcements": [fallback],
|
||||
"require_attention": False,
|
||||
}
|
||||
|
||||
|
||||
def display_announcements(console: Console, data: dict) -> None:
|
||||
"""Display announcements panel. Prompts for Enter if require_attention is True."""
|
||||
announcements = data.get("announcements", [])
|
||||
require_attention = data.get("require_attention", False)
|
||||
|
||||
if not announcements:
|
||||
return
|
||||
|
||||
content = "\n".join(announcements)
|
||||
|
||||
panel = Panel(
|
||||
content,
|
||||
border_style="cyan",
|
||||
padding=(1, 2),
|
||||
title="Announcements",
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
if require_attention:
|
||||
getpass.getpass("Press Enter to continue...")
|
||||
else:
|
||||
console.print()
|
||||
@@ -0,0 +1,6 @@
|
||||
CLI_CONFIG = {
|
||||
# Announcements
|
||||
"announcements_url": "https://api.tauric.ai/v1/announcements",
|
||||
"announcements_timeout": 1.0,
|
||||
"announcements_fallback": "[cyan]For more information, please visit[/cyan] [link=https://github.com/TauricResearch]https://github.com/TauricResearch[/link]",
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Dict
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AnalystType(str, Enum):
|
||||
MARKET = "market"
|
||||
SOCIAL = "social"
|
||||
NEWS = "news"
|
||||
FUNDAMENTALS = "fundamentals"
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
______ ___ ___ __
|
||||
/_ __/________ _____/ (_)___ ____ _/ | ____ ____ ____ / /______
|
||||
/ / / ___/ __ `/ __ / / __ \/ __ `/ /| |/ __ `/ _ \/ __ \/ __/ ___/
|
||||
/ / / / / /_/ / /_/ / / / / / /_/ / ___ / /_/ / __/ / / / /_(__ )
|
||||
/_/ /_/ \__,_/\__,_/_/_/ /_/\__, /_/ |_\__, /\___/_/ /_/\__/____/
|
||||
/____/ /____/
|
||||
@@ -0,0 +1,119 @@
|
||||
import threading
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from langchain_core.callbacks import BaseCallbackHandler
|
||||
from langchain_core.outputs import LLMResult
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
|
||||
def _pick_int(d: Optional[Dict[str, Any]], *keys: str) -> int:
|
||||
if not d:
|
||||
return 0
|
||||
for k in keys:
|
||||
v = d.get(k)
|
||||
if v is not None:
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return 0
|
||||
|
||||
|
||||
def _extract_tokens_from_usage_dict(u: Dict[str, Any]) -> Tuple[int, int]:
|
||||
"""兼容 OpenAI / Anthropic / Gemini 等字段命名。"""
|
||||
tin = _pick_int(u, "input_tokens", "prompt_tokens", "cache_read_input_tokens")
|
||||
tout = _pick_int(u, "output_tokens", "completion_tokens", "candidates_token_count")
|
||||
return tin, tout
|
||||
|
||||
|
||||
class StatsCallbackHandler(BaseCallbackHandler):
|
||||
"""Callback handler that tracks LLM calls, tool calls, and token usage."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._lock = threading.Lock()
|
||||
self.llm_calls = 0
|
||||
self.tool_calls = 0
|
||||
self.tokens_in = 0
|
||||
self.tokens_out = 0
|
||||
|
||||
def on_llm_start(
|
||||
self,
|
||||
serialized: Dict[str, Any],
|
||||
prompts: List[str],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Increment LLM call counter when an LLM starts."""
|
||||
with self._lock:
|
||||
self.llm_calls += 1
|
||||
|
||||
def on_chat_model_start(
|
||||
self,
|
||||
serialized: Dict[str, Any],
|
||||
messages: List[List[Any]],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Increment LLM call counter when a chat model starts."""
|
||||
with self._lock:
|
||||
self.llm_calls += 1
|
||||
|
||||
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
|
||||
"""Extract token usage from LLM response."""
|
||||
tin, tout = 0, 0
|
||||
try:
|
||||
generation = response.generations[0][0]
|
||||
except (IndexError, TypeError):
|
||||
generation = None
|
||||
|
||||
if generation is not None and hasattr(generation, "message"):
|
||||
message = generation.message
|
||||
if isinstance(message, AIMessage):
|
||||
if hasattr(message, "usage_metadata") and message.usage_metadata:
|
||||
um = message.usage_metadata
|
||||
if isinstance(um, dict):
|
||||
tin = _pick_int(um, "input_tokens", "prompt_tokens")
|
||||
tout = _pick_int(um, "output_tokens", "completion_tokens")
|
||||
if tin == 0 and tout == 0 and hasattr(message, "response_metadata"):
|
||||
rm = message.response_metadata or {}
|
||||
u = rm.get("token_usage") or rm.get("usage_metadata") or rm.get("usage")
|
||||
if isinstance(u, dict):
|
||||
tin, tout = _extract_tokens_from_usage_dict(u)
|
||||
|
||||
if tin == 0 and tout == 0 and generation is not None:
|
||||
gen_info = getattr(generation, "generation_info", None) or {}
|
||||
if isinstance(gen_info, dict):
|
||||
u = gen_info.get("token_usage") or gen_info.get("usage")
|
||||
if isinstance(u, dict):
|
||||
tin, tout = _extract_tokens_from_usage_dict(u)
|
||||
|
||||
if tin == 0 and tout == 0:
|
||||
llm_out = getattr(response, "llm_output", None)
|
||||
if isinstance(llm_out, dict):
|
||||
u = llm_out.get("token_usage") or llm_out.get("usage")
|
||||
if isinstance(u, dict):
|
||||
tin, tout = _extract_tokens_from_usage_dict(u)
|
||||
|
||||
if tin or tout:
|
||||
with self._lock:
|
||||
self.tokens_in += tin
|
||||
self.tokens_out += tout
|
||||
|
||||
def on_tool_start(
|
||||
self,
|
||||
serialized: Dict[str, Any],
|
||||
input_str: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Increment tool call counter when a tool starts."""
|
||||
with self._lock:
|
||||
self.tool_calls += 1
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Return current statistics."""
|
||||
with self._lock:
|
||||
return {
|
||||
"llm_calls": self.llm_calls,
|
||||
"tool_calls": self.tool_calls,
|
||||
"tokens_in": self.tokens_in,
|
||||
"tokens_out": self.tokens_out,
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
import questionary
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from cli.models import AnalystType
|
||||
from tradingagents.llm_clients.model_catalog import get_model_options
|
||||
|
||||
console = Console()
|
||||
|
||||
TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK"
|
||||
|
||||
ANALYST_ORDER = [
|
||||
("Market Analyst", AnalystType.MARKET),
|
||||
("Social Media Analyst", AnalystType.SOCIAL),
|
||||
("News Analyst", AnalystType.NEWS),
|
||||
("Fundamentals Analyst", AnalystType.FUNDAMENTALS),
|
||||
]
|
||||
|
||||
|
||||
def get_ticker() -> str:
|
||||
"""Prompt the user to enter a ticker symbol."""
|
||||
ticker = questionary.text(
|
||||
f"Enter the exact ticker symbol to analyze ({TICKER_INPUT_EXAMPLES}):",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a valid ticker symbol.",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("text", "fg:green"),
|
||||
("highlighted", "noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if not ticker:
|
||||
console.print("\n[red]No ticker symbol provided. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return normalize_ticker_symbol(ticker)
|
||||
|
||||
|
||||
def normalize_ticker_symbol(ticker: str) -> str:
|
||||
"""Normalize ticker input while preserving exchange suffixes."""
|
||||
return ticker.strip().upper()
|
||||
|
||||
|
||||
def get_analysis_date() -> str:
|
||||
"""Prompt the user to enter a date in YYYY-MM-DD format."""
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
def validate_date(date_str: str) -> bool:
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
|
||||
return False
|
||||
try:
|
||||
datetime.strptime(date_str, "%Y-%m-%d")
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
date = questionary.text(
|
||||
"Enter the analysis date (YYYY-MM-DD):",
|
||||
validate=lambda x: validate_date(x.strip())
|
||||
or "Please enter a valid date in YYYY-MM-DD format.",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("text", "fg:green"),
|
||||
("highlighted", "noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if not date:
|
||||
console.print("\n[red]No date provided. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return date.strip()
|
||||
|
||||
|
||||
def select_analysts() -> List[AnalystType]:
|
||||
"""Select analysts using an interactive checkbox."""
|
||||
choices = questionary.checkbox(
|
||||
"Select Your [Analysts Team]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value) for display, value in ANALYST_ORDER
|
||||
],
|
||||
instruction="\n- Press Space to select/unselect analysts\n- Press 'a' to select/unselect all\n- Press Enter when done",
|
||||
validate=lambda x: len(x) > 0 or "You must select at least one analyst.",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("checkbox-selected", "fg:green"),
|
||||
("selected", "fg:green noinherit"),
|
||||
("highlighted", "noinherit"),
|
||||
("pointer", "noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if not choices:
|
||||
console.print("\n[red]No analysts selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
def select_research_depth() -> int:
|
||||
"""Select research depth using an interactive selection."""
|
||||
|
||||
# Define research depth options with their corresponding values
|
||||
DEPTH_OPTIONS = [
|
||||
("Shallow - Quick research, few debate and strategy discussion rounds", 1),
|
||||
("Medium - Middle ground, moderate debate rounds and strategy discussion", 3),
|
||||
("Deep - Comprehensive research, in depth debate and strategy discussion", 5),
|
||||
]
|
||||
|
||||
choice = questionary.select(
|
||||
"Select Your [Research Depth]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value) for display, value in DEPTH_OPTIONS
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("selected", "fg:yellow noinherit"),
|
||||
("highlighted", "fg:yellow noinherit"),
|
||||
("pointer", "fg:yellow noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if choice is None:
|
||||
console.print("\n[red]No research depth selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
|
||||
def select_shallow_thinking_agent(provider) -> str:
|
||||
"""Select shallow thinking llm engine using an interactive selection."""
|
||||
|
||||
choice = questionary.select(
|
||||
"Select Your [Quick-Thinking LLM Engine]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value)
|
||||
for display, value in get_model_options(provider, "quick")
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("selected", "fg:magenta noinherit"),
|
||||
("highlighted", "fg:magenta noinherit"),
|
||||
("pointer", "fg:magenta noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if choice is None:
|
||||
console.print(
|
||||
"\n[red]No shallow thinking llm engine selected. Exiting...[/red]"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
|
||||
def select_deep_thinking_agent(provider) -> str:
|
||||
"""Select deep thinking llm engine using an interactive selection."""
|
||||
|
||||
choice = questionary.select(
|
||||
"Select Your [Deep-Thinking LLM Engine]:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=value)
|
||||
for display, value in get_model_options(provider, "deep")
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("selected", "fg:magenta noinherit"),
|
||||
("highlighted", "fg:magenta noinherit"),
|
||||
("pointer", "fg:magenta noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if choice is None:
|
||||
console.print("\n[red]No deep thinking llm engine selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
def select_llm_provider() -> tuple[str, str]:
|
||||
"""Select the OpenAI api url using interactive selection."""
|
||||
# Define OpenAI api options with their corresponding endpoints
|
||||
BASE_URLS = [
|
||||
("OpenAI", "https://api.openai.com/v1"),
|
||||
("SiliconFlow", "https://api.siliconflow.cn/v1"),
|
||||
("Google", "https://generativelanguage.googleapis.com/v1"),
|
||||
("Anthropic", "https://api.anthropic.com/"),
|
||||
("xAI", "https://api.x.ai/v1"),
|
||||
("Openrouter", "https://openrouter.ai/api/v1"),
|
||||
("Ollama", "http://localhost:11434/v1"),
|
||||
]
|
||||
|
||||
choice = questionary.select(
|
||||
"Select your LLM Provider:",
|
||||
choices=[
|
||||
questionary.Choice(display, value=(display, value))
|
||||
for display, value in BASE_URLS
|
||||
],
|
||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||
style=questionary.Style(
|
||||
[
|
||||
("selected", "fg:magenta noinherit"),
|
||||
("highlighted", "fg:magenta noinherit"),
|
||||
("pointer", "fg:magenta noinherit"),
|
||||
]
|
||||
),
|
||||
).ask()
|
||||
|
||||
if choice is None:
|
||||
console.print("\n[red]no OpenAI backend selected. Exiting...[/red]")
|
||||
exit(1)
|
||||
|
||||
display_name, url = choice
|
||||
print(f"You selected: {display_name}\tURL: {url}")
|
||||
|
||||
return display_name, url
|
||||
|
||||
|
||||
def ask_openai_reasoning_effort() -> str:
|
||||
"""Ask for OpenAI reasoning effort level."""
|
||||
choices = [
|
||||
questionary.Choice("Medium (Default)", "medium"),
|
||||
questionary.Choice("High (More thorough)", "high"),
|
||||
questionary.Choice("Low (Faster)", "low"),
|
||||
]
|
||||
return questionary.select(
|
||||
"Select Reasoning Effort:",
|
||||
choices=choices,
|
||||
style=questionary.Style([
|
||||
("selected", "fg:cyan noinherit"),
|
||||
("highlighted", "fg:cyan noinherit"),
|
||||
("pointer", "fg:cyan noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
|
||||
def ask_anthropic_effort() -> str | None:
|
||||
"""Ask for Anthropic effort level.
|
||||
|
||||
Controls token usage and response thoroughness on Claude 4.5+ and 4.6 models.
|
||||
"""
|
||||
return questionary.select(
|
||||
"Select Effort Level:",
|
||||
choices=[
|
||||
questionary.Choice("High (recommended)", "high"),
|
||||
questionary.Choice("Medium (balanced)", "medium"),
|
||||
questionary.Choice("Low (faster, cheaper)", "low"),
|
||||
],
|
||||
style=questionary.Style([
|
||||
("selected", "fg:cyan noinherit"),
|
||||
("highlighted", "fg:cyan noinherit"),
|
||||
("pointer", "fg:cyan noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
|
||||
def ask_gemini_thinking_config() -> str | None:
|
||||
"""Ask for Gemini thinking configuration.
|
||||
|
||||
Returns thinking_level: "high" or "minimal".
|
||||
Client maps to appropriate API param based on model series.
|
||||
"""
|
||||
return questionary.select(
|
||||
"Select Thinking Mode:",
|
||||
choices=[
|
||||
questionary.Choice("Enable Thinking (recommended)", "high"),
|
||||
questionary.Choice("Minimal/Disable Thinking", "minimal"),
|
||||
],
|
||||
style=questionary.Style([
|
||||
("selected", "fg:green noinherit"),
|
||||
("highlighted", "fg:green noinherit"),
|
||||
("pointer", "fg:green noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
|
||||
def ask_output_language() -> str:
|
||||
"""Ask for report output language."""
|
||||
choice = questionary.select(
|
||||
"Select Output Language:",
|
||||
choices=[
|
||||
questionary.Choice("English (default)", "English"),
|
||||
questionary.Choice("Chinese (中文)", "Chinese"),
|
||||
questionary.Choice("Japanese (日本語)", "Japanese"),
|
||||
questionary.Choice("Korean (한국어)", "Korean"),
|
||||
questionary.Choice("Hindi (हिन्दी)", "Hindi"),
|
||||
questionary.Choice("Spanish (Español)", "Spanish"),
|
||||
questionary.Choice("Portuguese (Português)", "Portuguese"),
|
||||
questionary.Choice("French (Français)", "French"),
|
||||
questionary.Choice("German (Deutsch)", "German"),
|
||||
questionary.Choice("Arabic (العربية)", "Arabic"),
|
||||
questionary.Choice("Russian (Русский)", "Russian"),
|
||||
questionary.Choice("Custom language", "custom"),
|
||||
],
|
||||
style=questionary.Style([
|
||||
("selected", "fg:yellow noinherit"),
|
||||
("highlighted", "fg:yellow noinherit"),
|
||||
("pointer", "fg:yellow noinherit"),
|
||||
]),
|
||||
).ask()
|
||||
|
||||
if choice == "custom":
|
||||
return questionary.text(
|
||||
"Enter language name (e.g. Turkish, Vietnamese, Thai, Indonesian):",
|
||||
validate=lambda x: len(x.strip()) > 0 or "Please enter a language name.",
|
||||
).ask().strip()
|
||||
|
||||
return choice
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"close_val": 633.1801147460938,
|
||||
"ma200_val": 596.570202331543,
|
||||
"ma50_val": 600.1725512695313,
|
||||
"a1_pass": true,
|
||||
"a1_metric": "QQQ = 633.18 | MA200 = 596.57",
|
||||
"a1_verdict": "日收盘价已站上 200 日均线,信号有效。",
|
||||
"a2_score": 1,
|
||||
"a2_metric": "MA200 变化:过去 20 个交易日 +1.07%(Rising)",
|
||||
"a2_verdict": "过去 20 个交易日上升 +1.07%,斜率向上 — 通过。",
|
||||
"a3_pass": true,
|
||||
"a3_metric": "连续 6 日收盘站上 MA200(至少需要 3 日)",
|
||||
"a3_verdict": "连续 3 日以上收盘站上 MA200,突破有效。",
|
||||
"b1_pass": false,
|
||||
"b1_metric": "Breakout vol: 63.1M | 20d avg: 66.6M | Ratio: 0.95x",
|
||||
"b1_verdict": "成交量低于均值,突破力度偏弱,建议减仓 25%。",
|
||||
"b2_pass": true,
|
||||
"b2_metric": "QQQ = 633.18 | MA50 = 600.17 | MA200 = 596.57",
|
||||
"b2_verdict": "价格 > MA50 > MA200 — 黄金交叉区间(强势)",
|
||||
"b3_pass": false,
|
||||
"days_since": 5,
|
||||
"cross_count": 1,
|
||||
"cross_idx": "2026-04-08",
|
||||
"signal_quality": "Weak",
|
||||
"_updated_at": "2026-04-16 01:31:58"
|
||||
}
|
||||
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"macro_score_1": 1.0,
|
||||
"macro_score_2": 1.0,
|
||||
"macro_score_3": 1.0,
|
||||
"macro_score_4": 1.0,
|
||||
"macro_score_5": 1.0,
|
||||
"macro_score_total": 5.0,
|
||||
"macro_tested": true,
|
||||
"_updated_at": "2026-04-26 03:28:14"
|
||||
}
|
||||
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"total_cash": 19000.0,
|
||||
"tqqq_price": 54.61989974975586,
|
||||
"macro_score": 4,
|
||||
"ceiling_pct": 1.0,
|
||||
"deployable": 19000.0,
|
||||
"quality_adj": 0.75,
|
||||
"deployable_adj": 14250.0,
|
||||
"direct_alloc": 4750.0,
|
||||
"wheel_margin": 4750.0,
|
||||
"shares": 86,
|
||||
"actual_cost": 4697.311378479004,
|
||||
"put_contracts": 0,
|
||||
"reserve_cash": 14302.688621520996,
|
||||
"entry_mode": "Split",
|
||||
"signal_quality": "Weak",
|
||||
"_updated_at": "2026-04-15 23:27:11"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"cross_idx": "2026-04-08",
|
||||
"cross_price": 48.0,
|
||||
"current_price": 54.67499923706055,
|
||||
"distance_pct": 13.906248410542807,
|
||||
"entry_mode": "Split",
|
||||
"mode_detail": "1/3 直接买入 + 1/3 Sell Put + 1/3 预留",
|
||||
"rationale": "距离 13.9% — TQQQ 已有一定涨幅,分批入场降低追高风险。",
|
||||
"macro_score": 5.0,
|
||||
"_updated_at": "2026-04-16 01:34:30"
|
||||
}
|
||||
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"start": "2025-04-24",
|
||||
"end": "2026-04-24",
|
||||
"metrics": {
|
||||
"total_return": 13.034483512918182,
|
||||
"final_value": 11303.448351291818,
|
||||
"cagr": 13.082052914455588,
|
||||
"sharpe": 2.8013245352313114,
|
||||
"max_dd": -2.075202619162914,
|
||||
"n_trades": 33,
|
||||
"wins": 31,
|
||||
"losses": 2,
|
||||
"win_rate": 93.93939393939394,
|
||||
"avg_win": 28.028074659204595,
|
||||
"avg_loss": -287.9317604510027,
|
||||
"chain_quotes": 0,
|
||||
"bs_quotes": 86
|
||||
},
|
||||
"n_trades": 33,
|
||||
"_updated_at": "2026-04-24 16:21:43"
|
||||
}
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"start": "2025-04-24",
|
||||
"end": "2026-04-24",
|
||||
"style": "neutral",
|
||||
"underlying_ticker": "TQQQ",
|
||||
"signal_ticker": "QQQ",
|
||||
"put_otm_pct": 9.0,
|
||||
"call_otm_pct": 6.0,
|
||||
"put_tp_pct": 50.0,
|
||||
"call_tp_pct": 50.0,
|
||||
"put_sl_pct": 200.0,
|
||||
"call_sl_pct": null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"total_return": 3.2482335125425754,
|
||||
"final_value": 10324.823351254257,
|
||||
"cagr": 3.259568016903991,
|
||||
"sharpe": 0.09896699284613594,
|
||||
"max_dd": -2.619957219409812,
|
||||
"n_trades": 9,
|
||||
"wins": 7,
|
||||
"losses": 2,
|
||||
"win_rate": 77.77777777777779,
|
||||
"avg_win": 55.310639861806635,
|
||||
"avg_loss": -181.81293525279108,
|
||||
"chain_quotes": 0,
|
||||
"bs_quotes": 37
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
[
|
||||
{
|
||||
"date": "2025-08-26 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 39.74,
|
||||
"spot": 45.12633514404297,
|
||||
"premium": 0.5536146925726833,
|
||||
"close_cost": 0.24599471329293987,
|
||||
"pnl": 24.781997927974338,
|
||||
"hold_days": 6,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-10-13 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 44.03,
|
||||
"spot": 51.44924545288086,
|
||||
"premium": 0.796619024896291,
|
||||
"close_cost": 0.3509533977764301,
|
||||
"pnl": 38.58656271198609,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-11-10 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 48.48,
|
||||
"spot": 56.17401885986328,
|
||||
"premium": 1.778766330702327,
|
||||
"close_cost": 0.48237271307612684,
|
||||
"pnl": 123.65936176262001,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-12-19 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 47.91,
|
||||
"spot": 53.34812545776367,
|
||||
"premium": 1.036592731680047,
|
||||
"close_cost": 0.46879731298197225,
|
||||
"pnl": 50.79954186980747,
|
||||
"hold_days": 7,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-06 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 47.56,
|
||||
"spot": 54.91919708251953,
|
||||
"premium": 0.5702405787526548,
|
||||
"close_cost": 0.19268997243538966,
|
||||
"pnl": 31.775060631726507,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-05 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "SL_Close",
|
||||
"strike": 47.71,
|
||||
"spot": 47.561363220214844,
|
||||
"premium": 0.721505715176253,
|
||||
"close_cost": 2.419175603087396,
|
||||
"pnl": -175.74698879111432,
|
||||
"hold_days": 2,
|
||||
"result": "Loss"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-05 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 45.23,
|
||||
"spot": 49.717796325683594,
|
||||
"premium": 1.061063730657704,
|
||||
"close_cost": 0.5197564837331896,
|
||||
"pnl": 48.15072469245143,
|
||||
"hold_days": 9,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-20 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "SL_Close",
|
||||
"strike": 43.75,
|
||||
"spot": 43.00889205932617,
|
||||
"premium": 0.8460148560320633,
|
||||
"close_cost": 2.6650036731767415,
|
||||
"pnl": -187.8788817144678,
|
||||
"hold_days": 3,
|
||||
"result": "Loss"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-09 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 40.18,
|
||||
"spot": 48.959999084472656,
|
||||
"premium": 1.4108038692109268,
|
||||
"close_cost": 0.6567915748501205,
|
||||
"pnl": 69.42122943608062,
|
||||
"hold_days": 2,
|
||||
"result": "Win"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"start": "2025-04-24",
|
||||
"end": "2026-04-24",
|
||||
"style": "neutral",
|
||||
"underlying_ticker": "TQQQ",
|
||||
"signal_ticker": "QQQ",
|
||||
"put_otm_pct": 9.0,
|
||||
"call_otm_pct": 6.0,
|
||||
"put_tp_pct": 80.0,
|
||||
"call_tp_pct": 50.0,
|
||||
"put_sl_pct": 200.0,
|
||||
"call_sl_pct": null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"total_return": 5.328146325585448,
|
||||
"final_value": 10532.814632558544,
|
||||
"cagr": 5.34692422213614,
|
||||
"sharpe": 0.5849740422340445,
|
||||
"max_dd": -2.5196350839124597,
|
||||
"n_trades": 9,
|
||||
"wins": 7,
|
||||
"losses": 2,
|
||||
"win_rate": 77.77777777777779,
|
||||
"avg_win": 84.73485015577744,
|
||||
"avg_loss": -181.8129352527916,
|
||||
"chain_quotes": 0,
|
||||
"bs_quotes": 69
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
[
|
||||
{
|
||||
"date": "2025-09-03 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 39.74,
|
||||
"spot": 44.36949920654297,
|
||||
"premium": 0.5536159877961406,
|
||||
"close_cost": 0.03495383585154532,
|
||||
"pnl": 45.886215194459524,
|
||||
"hold_days": 14,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-10-20 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 44.03,
|
||||
"spot": 53.771759033203125,
|
||||
"premium": 0.7966187749534139,
|
||||
"close_cost": 0.05749347645129843,
|
||||
"pnl": 67.93252985021155,
|
||||
"hold_days": 10,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-11-25 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 48.48,
|
||||
"spot": 51.84297180175781,
|
||||
"premium": 1.778766330702327,
|
||||
"close_cost": 0.1473346610213957,
|
||||
"pnl": 157.16316696809312,
|
||||
"hold_days": 19,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-12-23 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 47.91,
|
||||
"spot": 54.83333969116211,
|
||||
"premium": 1.0365927316800434,
|
||||
"close_cost": 0.06827979046142785,
|
||||
"pnl": 90.85129412186156,
|
||||
"hold_days": 11,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-09 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 47.56,
|
||||
"spot": 55.6679573059082,
|
||||
"premium": 0.5702405787526459,
|
||||
"close_cost": 0.10914421850859846,
|
||||
"pnl": 40.129636024404746,
|
||||
"hold_days": 7,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-05 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "SL_Close",
|
||||
"strike": 47.71,
|
||||
"spot": 47.561363220214844,
|
||||
"premium": 0.7215057151762458,
|
||||
"close_cost": 2.419175603087396,
|
||||
"pnl": -175.746988791115,
|
||||
"hold_days": 2,
|
||||
"result": "Loss"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-11 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 45.23,
|
||||
"spot": 49.26853942871094,
|
||||
"premium": 1.0610637306577004,
|
||||
"close_cost": 0.17795843012527435,
|
||||
"pnl": 82.33053005324261,
|
||||
"hold_days": 15,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-20 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "SL_Close",
|
||||
"strike": 43.75,
|
||||
"spot": 43.00889205932617,
|
||||
"premium": 0.8460148560320597,
|
||||
"close_cost": 2.6650036731767415,
|
||||
"pnl": -187.87888171446818,
|
||||
"hold_days": 3,
|
||||
"result": "Loss"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-13 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 40.18,
|
||||
"spot": 50.65999984741211,
|
||||
"premium": 1.4108038692109268,
|
||||
"close_cost": 0.26249808042923695,
|
||||
"pnl": 108.85057887816899,
|
||||
"hold_days": 6,
|
||||
"result": "Win"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"start": "2025-04-24",
|
||||
"end": "2026-04-24",
|
||||
"style": "conservative",
|
||||
"underlying_ticker": "TQQQ",
|
||||
"signal_ticker": "QQQ",
|
||||
"put_otm_pct": 12.0,
|
||||
"call_otm_pct": 0.0,
|
||||
"put_tp_pct": 50.0,
|
||||
"call_tp_pct": 50.0,
|
||||
"put_sl_pct": 150.0,
|
||||
"call_sl_pct": null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"total_return": 5.458408550097382,
|
||||
"final_value": 10545.84085500974,
|
||||
"cagr": 5.477657356105814,
|
||||
"sharpe": 2.031983303515009,
|
||||
"max_dd": -0.6003518878697439,
|
||||
"n_trades": 3,
|
||||
"wins": 3,
|
||||
"losses": 0,
|
||||
"win_rate": 100.0,
|
||||
"avg_win": 81.69156838518819,
|
||||
"avg_loss": 0.0,
|
||||
"chain_quotes": 0,
|
||||
"bs_quotes": 21
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"date": "2025-11-26 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 41.65,
|
||||
"spot": 53.198604583740234,
|
||||
"premium": 2.066391470619516,
|
||||
"close_cost": 0.9709011820930149,
|
||||
"pnl": 103.56902885265012,
|
||||
"hold_days": 5,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-20 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 41.85,
|
||||
"spot": 49.97737121582031,
|
||||
"premium": 1.264905721711207,
|
||||
"close_cost": 0.5412533437233309,
|
||||
"pnl": 66.3852377987876,
|
||||
"hold_days": 15,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-06 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 34.13,
|
||||
"spot": 44.099998474121094,
|
||||
"premium": 1.4551086033202925,
|
||||
"close_cost": 0.6441042182790238,
|
||||
"pnl": 75.12043850412687,
|
||||
"hold_days": 10,
|
||||
"result": "Win"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"start": "2021-04-24",
|
||||
"end": "2026-04-24",
|
||||
"style": "aggressive",
|
||||
"underlying_ticker": "TQQQ",
|
||||
"signal_ticker": "QQQ",
|
||||
"put_otm_pct": 6.0,
|
||||
"call_otm_pct": 2.0,
|
||||
"put_tp_pct": 80.0,
|
||||
"call_tp_pct": 80.0,
|
||||
"put_sl_pct": null,
|
||||
"call_sl_pct": null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"total_return": 22.733742227955787,
|
||||
"final_value": 12273.374222795579,
|
||||
"cagr": 4.189634814548815,
|
||||
"sharpe": 0.1856543053754313,
|
||||
"max_dd": -12.24209072332098,
|
||||
"n_trades": 172,
|
||||
"wins": 134,
|
||||
"losses": 38,
|
||||
"win_rate": 77.90697674418605,
|
||||
"avg_win": 47.0994160151811,
|
||||
"avg_loss": -164.8441877629063,
|
||||
"chain_quotes": 0,
|
||||
"bs_quotes": 771
|
||||
}
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"start": "2021-04-24",
|
||||
"end": "2026-04-24",
|
||||
"style": "aggressive",
|
||||
"underlying_ticker": "TQQQ",
|
||||
"signal_ticker": "QQQ",
|
||||
"put_otm_pct": 6.0,
|
||||
"call_otm_pct": 2.0,
|
||||
"put_tp_pct": 50.0,
|
||||
"call_tp_pct": 80.0,
|
||||
"put_sl_pct": null,
|
||||
"call_sl_pct": null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"total_return": 21.203246816790934,
|
||||
"final_value": 12120.324681679094,
|
||||
"cagr": 3.928013990277779,
|
||||
"sharpe": 0.15407662226007332,
|
||||
"max_dd": -12.80745424951754,
|
||||
"n_trades": 167,
|
||||
"wins": 137,
|
||||
"losses": 30,
|
||||
"win_rate": 82.03592814371258,
|
||||
"avg_win": 40.5684761051716,
|
||||
"avg_loss": -146.16866231788606,
|
||||
"chain_quotes": 0,
|
||||
"bs_quotes": 649
|
||||
}
|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"start": "2025-04-24",
|
||||
"end": "2026-04-24",
|
||||
"style": "aggressive",
|
||||
"underlying_ticker": "TQQQ",
|
||||
"signal_ticker": "QQQ",
|
||||
"put_otm_pct": 6.0,
|
||||
"call_otm_pct": 2.0,
|
||||
"put_tp_pct": 50.0,
|
||||
"call_tp_pct": 80.0,
|
||||
"put_sl_pct": null,
|
||||
"call_sl_pct": null,
|
||||
"engine_version": "wheel-v3-split",
|
||||
"rule_version": "tqqq-wheel-manual-v1.1"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"total_return": 13.682463255870015,
|
||||
"final_value": 11368.246325587003,
|
||||
"cagr": 13.73253788517028,
|
||||
"sharpe": 2.857837373795672,
|
||||
"max_dd": -2.073090936612936,
|
||||
"n_trades": 36,
|
||||
"wins": 34,
|
||||
"losses": 2,
|
||||
"win_rate": 94.44444444444444,
|
||||
"avg_win": 27.45038810795333,
|
||||
"avg_loss": -287.9317604510025,
|
||||
"chain_quotes": 0,
|
||||
"bs_quotes": 98
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
[
|
||||
{
|
||||
"date": "2025-04-25 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 24.34,
|
||||
"spot": 26.743024826049805,
|
||||
"premium": 0.29771831494625456,
|
||||
"close_cost": 0.11768716878504693,
|
||||
"pnl": 12.023114616120761,
|
||||
"hold_days": 1,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-06-23 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 34.33,
|
||||
"spot": 37.09067153930664,
|
||||
"premium": 0.25507430657319397,
|
||||
"close_cost": 0.0003134096309214199,
|
||||
"pnl": 19.496089694227255,
|
||||
"hold_days": 6,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-08-04 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 38.81,
|
||||
"spot": 43.547935485839844,
|
||||
"premium": 0.07631244215648936,
|
||||
"close_cost": 0.0012067786162033162,
|
||||
"pnl": 1.530566354028604,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-08-12 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 42.76,
|
||||
"spot": 47.16282272338867,
|
||||
"premium": 0.20387439304942667,
|
||||
"close_cost": 0.04814236063376276,
|
||||
"pnl": 9.59320324156639,
|
||||
"hold_days": 1,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-08-25 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 43.59,
|
||||
"spot": 44.60850143432617,
|
||||
"premium": 0.23350383928957275,
|
||||
"close_cost": 0.0,
|
||||
"pnl": 17.370383928957274,
|
||||
"hold_days": 7,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-08-27 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 41.93,
|
||||
"spot": 45.330482482910156,
|
||||
"premium": 0.33365346119653694,
|
||||
"close_cost": 0.12654794173393213,
|
||||
"pnl": 14.73055194626048,
|
||||
"hold_days": 2,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-09-03 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 40.76,
|
||||
"spot": 44.36949920654297,
|
||||
"premium": 0.20378308864153194,
|
||||
"close_cost": 0.06833610823069369,
|
||||
"pnl": 7.564698041083824,
|
||||
"hold_days": 1,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-09-11 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 43.94,
|
||||
"spot": 47.58605194091797,
|
||||
"premium": 0.18230798811836024,
|
||||
"close_cost": 0.03431214511036429,
|
||||
"pnl": 8.819584300799594,
|
||||
"hold_days": 2,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-09-26 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 46.87,
|
||||
"spot": 50.46242141723633,
|
||||
"premium": 0.09773871361597575,
|
||||
"close_cost": 0.03636038484441828,
|
||||
"pnl": 0.15783287715574623,
|
||||
"hold_days": 1,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-10-13 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 45.48,
|
||||
"spot": 51.44924545288086,
|
||||
"premium": 0.3973731113950638,
|
||||
"close_cost": 0.02545040672940546,
|
||||
"pnl": 31.212270466565837,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-10-20 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 48.72,
|
||||
"spot": 53.771759033203125,
|
||||
"premium": 0.5350110743825365,
|
||||
"close_cost": 0.08535188421012219,
|
||||
"pnl": 38.98591901724143,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-10-27 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 51.8,
|
||||
"spot": 58.028045654296875,
|
||||
"premium": 0.6423018604945661,
|
||||
"close_cost": 0.07661382961036578,
|
||||
"pnl": 50.58880308842002,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-11-03 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 54.68,
|
||||
"spot": 58.97001266479492,
|
||||
"premium": 0.832744356436832,
|
||||
"close_cost": 0.30740027044818063,
|
||||
"pnl": 46.55440859886514,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-11-10 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 49.56,
|
||||
"spot": 56.17401885986328,
|
||||
"premium": 0.6046077804997161,
|
||||
"close_cost": 0.03662807947514746,
|
||||
"pnl": 50.81797010245687,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-11-21 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "Assigned",
|
||||
"strike": 49.07,
|
||||
"spot": 47.327518463134766,
|
||||
"premium": 0.6957166706715263,
|
||||
"close_cost": 1.7424815368652347,
|
||||
"pnl": -107.66648661937083,
|
||||
"hold_days": 7,
|
||||
"result": "Loss"
|
||||
},
|
||||
{
|
||||
"date": "2025-11-28 00:00:00",
|
||||
"type": "CALL",
|
||||
"action": "Call_Away",
|
||||
"strike": 48.27,
|
||||
"spot": 54.36484909057617,
|
||||
"premium": 1.442778747749827,
|
||||
"close_cost": 6.094849090576169,
|
||||
"pnl": -468.19703428263415,
|
||||
"hold_days": 7,
|
||||
"result": "Loss"
|
||||
},
|
||||
{
|
||||
"date": "2025-12-02 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 51.1,
|
||||
"spot": 55.12240982055664,
|
||||
"premium": 0.8885543779888518,
|
||||
"close_cost": 0.22771800847670498,
|
||||
"pnl": 60.10363695121468,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-12-09 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 52.61,
|
||||
"spot": 55.83012390136719,
|
||||
"premium": 0.7019645425702308,
|
||||
"close_cost": 0.1930723601146358,
|
||||
"pnl": 44.9092182455595,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-12-16 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 49.49,
|
||||
"spot": 52.161949157714844,
|
||||
"premium": 0.5255269728821705,
|
||||
"close_cost": 0.22041313720210542,
|
||||
"pnl": 24.53138356800651,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2025-12-22 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 50.15,
|
||||
"spot": 54.075782775878906,
|
||||
"premium": 0.47745539216248645,
|
||||
"close_cost": 0.13864091225912745,
|
||||
"pnl": 27.9014479903359,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-02 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 51.91,
|
||||
"spot": 52.263587951660156,
|
||||
"premium": 0.2968084430280751,
|
||||
"close_cost": 0.0,
|
||||
"pnl": 23.700844302807507,
|
||||
"hold_days": 7,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-05 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 49.13,
|
||||
"spot": 53.51152420043945,
|
||||
"premium": 0.27308307351277783,
|
||||
"close_cost": 0.041062341360688315,
|
||||
"pnl": 17.22207321520895,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-12 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 52.33,
|
||||
"spot": 55.79774475097656,
|
||||
"premium": 0.369411761450964,
|
||||
"close_cost": 0.14436308920966834,
|
||||
"pnl": 16.524867224129565,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-22 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 50.81,
|
||||
"spot": 53.811031341552734,
|
||||
"premium": 0.1477798974439022,
|
||||
"close_cost": 0.0022624023227785783,
|
||||
"pnl": 8.57174951211236,
|
||||
"hold_days": 6,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-01-26 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 51.03,
|
||||
"spot": 54.98908615112305,
|
||||
"premium": 0.2433979224470182,
|
||||
"close_cost": 0.050428038206870696,
|
||||
"pnl": 13.316988424014749,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-02 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 50.68,
|
||||
"spot": 55.00904846191406,
|
||||
"premium": 0.29612130729873165,
|
||||
"close_cost": 0.05015753030630954,
|
||||
"pnl": 18.61637769924221,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-09 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 47.48,
|
||||
"spot": 51.654598236083984,
|
||||
"premium": 0.563188229925693,
|
||||
"close_cost": 0.13619518544464437,
|
||||
"pnl": 36.71930444810485,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-17 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 45.49,
|
||||
"spot": 48.27019119262695,
|
||||
"premium": 0.5999675089350056,
|
||||
"close_cost": 0.220282559906253,
|
||||
"pnl": 31.988494902875264,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-24 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 46.98,
|
||||
"spot": 49.69783020019531,
|
||||
"premium": 0.4884002354982204,
|
||||
"close_cost": 0.21242084272299522,
|
||||
"pnl": 21.61793927752252,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-02 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 46.47,
|
||||
"spot": 49.61796569824219,
|
||||
"premium": 0.5888532941584685,
|
||||
"close_cost": 0.25315686325419495,
|
||||
"pnl": 27.589643090427362,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-09 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 44.61,
|
||||
"spot": 49.308475494384766,
|
||||
"premium": 0.4907682450346762,
|
||||
"close_cost": 0.04880422279325147,
|
||||
"pnl": 38.21640222414247,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-16 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 43.1,
|
||||
"spot": 47.38166046142578,
|
||||
"premium": 0.36589208886678115,
|
||||
"close_cost": 0.05541615318789983,
|
||||
"pnl": 25.06759356788813,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-23 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 40.43,
|
||||
"spot": 44.496429443359375,
|
||||
"premium": 0.4571711027535077,
|
||||
"close_cost": 0.07888536647136002,
|
||||
"pnl": 31.84857362821477,
|
||||
"hold_days": 3,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-03-31 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 36.45,
|
||||
"spot": 41.68000030517578,
|
||||
"premium": 0.4899103648679617,
|
||||
"close_cost": 0.03275982095700081,
|
||||
"pnl": 39.735054391096085,
|
||||
"hold_days": 4,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-08 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 41.45,
|
||||
"spot": 48.0,
|
||||
"premium": 0.782588368441985,
|
||||
"close_cost": 0.12738446714409601,
|
||||
"pnl": 59.54039012978889,
|
||||
"hold_days": 2,
|
||||
"result": "Win"
|
||||
},
|
||||
{
|
||||
"date": "2026-04-14 00:00:00",
|
||||
"type": "PUT",
|
||||
"action": "TP_Close",
|
||||
"strike": 47.62,
|
||||
"spot": 53.40999984741211,
|
||||
"premium": 1.0000270235383653,
|
||||
"close_cost": 0.3787688574986472,
|
||||
"pnl": 56.14581660397181,
|
||||
"hold_days": 1,
|
||||
"result": "Win"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 166 KiB |