feat: 增加初始代码;目前已正常连接sdk,可获取各周期股票数据

master
Lxy 2 months ago
commit 654d641547

63
.gitignore vendored

@ -0,0 +1,63 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Environment variables
.env
.env.local
.env.*.local
# Database
*.db
*.sqlite3
# Logs
*.log
logs/
# Frontend
node_modules/
dist/
build/
.npm
.yarn
# Docker
.dockerignore
# Misc
.DS_Store
Thumbs.db

@ -0,0 +1,904 @@
# AmazingData 金融数据服务平台 - 完整实现方案
## 一、项目概述
基于AmazingData SDK构建的金融数据服务平台提供历史数据缓存、实时数据订阅、批量数据管理等功能。
---
## 二、技术架构
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 前端层 (Frontend) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 登录页面 │ │ 配置管理页面 │ │ 数据查询页面 │ │ 测试中心页面 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ API网关层 (API Gateway) │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FastAPI + Uvicorn │ JWT认证 │ 限流/熔断 │ 日志/监控 │ CORS │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 业务服务层 (Services) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 认证服务 │ │ 配置服务 │ │ 历史数据服务 │ │ 实时数据服务 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 缓存管理服务 │ │ 财务数据服务 │ │ 基础数据服务 │ │ 测试服务 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 数据访问层 (Data Access) │
│ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │
│ │ SQLAlchemy ORM │ │ Redis Cache │ │ AmazingData SDK │ │
│ └────────────────────┘ └────────────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 数据存储层 (Storage) │
│ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ File System │ │
│ │ (主数据库) │ │ (缓存/消息队列) │ │ (日志/导出) │ │
│ └────────────────────┘ └────────────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
---
## 三、技术栈选型
| 层级 | 技术 | 版本 | 说明 |
|------|------|------|------|
| 后端框架 | FastAPI | ^0.104.0 | 高性能异步Web框架 |
| ORM | SQLAlchemy | ^2.0.0 | 数据库ORM |
| 数据库 | PostgreSQL | 15+ | 主数据库 |
| 缓存 | Redis | 7+ | 缓存与消息队列 |
| 前端 | Vue3 + TypeScript | ^3.3.0 | 前端框架 |
| UI组件 | Element Plus | ^2.4.0 | UI组件库 |
| 图表 | ECharts | ^5.4.0 | K线图展示 |
| 认证 | JWT + bcrypt | - | 用户认证 |
| 任务调度 | APScheduler | ^3.10.0 | 定时任务 |
| SDK | AmazingData | 1.0.24 | 数据源SDK |
---
## 四、数据库设计
### 4.1 实体关系图 (ER Diagram)
```
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ users │ │ sdk_configs │ │ system_configs │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ username │ │ name │ │ key │
│ password_hash │ │ username │ │ value │
│ is_active │ │ password │ │ description │
│ created_at │ │ host │ │ created_at │
│ updated_at │ │ port │ │ updated_at │
└──────────────────┘ │ is_active │ └──────────────────┘
│ created_at │
└──────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│stock_kline_daily │ │ stock_kline_min │ │stock_kline_other │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ code │ │ code │ │ code │
│ trade_date │ │ trade_datetime │ │ period_type │
│ open │ │ open │ │ trade_date │
│ high │ │ high │ │ open │
│ low │ │ low │ │ high │
│ close │ │ close │ │ low │
│ volume │ │ volume │ │ close │
│ amount │ │ amount │ │ volume │
│ created_at │ │ created_at │ │ amount │
└──────────────────┘ └──────────────────┘ │ created_at │
└──────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│future_kline_daily│ │ future_kline_min │ │realtime_snapshot │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ code │ │ code │ │ code │
│ trade_date │ │ trade_datetime │ │ trade_time │
│ open │ │ open │ │ pre_close │
│ high │ │ high │ │ last │
│ low │ │ low │ │ open │
│ close │ │ close │ │ high │
│ volume │ │ volume │ │ low │
│ amount │ │ amount │ │ volume │
│ settle │ │ settle │ │ amount │
│ open_interest │ │ open_interest │ │ ...其他字段 │
│ created_at │ │ created_at │ │ created_at │
└──────────────────┘ └──────────────────┘ │ expires_at │
└──────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ cache_tasks │ │ cache_records │ │ api_test_logs │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ task_name │ │ task_id (FK) │ │ test_name │
│ task_type │ │ code │ │ api_endpoint │
│ security_type │ │ trade_date │ │ request_params │
│ start_date │ │ status │ │ response_data │
│ end_date │ │ record_count │ │ status_code │
│ status │ │ created_at │ │ execution_time │
│ progress │ └──────────────────┘ │ error_message │
│ total_count │ │ created_at │
│ success_count │ └──────────────────┘
│ error_count │
│ created_at │
│ completed_at │
└──────────────────┘
```
### 4.2 表结构详细定义
#### 4.2.1 用户表 (users)
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
#### 4.2.2 SDK配置表 (sdk_configs)
```sql
CREATE TABLE sdk_configs (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
username VARCHAR(100) NOT NULL,
password VARCHAR(255) NOT NULL,
host VARCHAR(100) NOT NULL,
port INTEGER NOT NULL,
local_path VARCHAR(255) DEFAULT './amazing_data_cache/',
is_active BOOLEAN DEFAULT TRUE,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
#### 4.2.3 股票日线数据表 (stock_kline_daily)
```sql
CREATE TABLE stock_kline_daily (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, trade_date)
);
CREATE INDEX idx_stock_daily_code_date ON stock_kline_daily(code, trade_date);
CREATE INDEX idx_stock_daily_trade_date ON stock_kline_daily(trade_date);
```
#### 4.2.4 股票分钟数据表 (stock_kline_min)
```sql
CREATE TABLE stock_kline_min (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
period_type VARCHAR(10) NOT NULL, -- min1, min5, min15, min30, min60
trade_datetime TIMESTAMP NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, period_type, trade_datetime)
);
CREATE INDEX idx_stock_min_code_period_datetime ON stock_kline_min(code, period_type, trade_datetime);
CREATE INDEX idx_stock_min_trade_datetime ON stock_kline_min(trade_datetime);
```
#### 4.2.5 期货日线数据表 (future_kline_daily)
```sql
CREATE TABLE future_kline_daily (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
settle DECIMAL(12, 4),
open_interest BIGINT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, trade_date)
);
CREATE INDEX idx_future_daily_code_date ON future_kline_daily(code, trade_date);
CREATE INDEX idx_future_daily_trade_date ON future_kline_daily(trade_date);
```
#### 4.2.6 期货分钟数据表 (future_kline_min)
```sql
CREATE TABLE future_kline_min (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
period_type VARCHAR(10) NOT NULL,
trade_datetime TIMESTAMP NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
settle DECIMAL(12, 4),
open_interest BIGINT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, period_type, trade_datetime)
);
CREATE INDEX idx_future_min_code_period_datetime ON future_kline_min(code, period_type, trade_datetime);
```
#### 4.2.7 实时快照数据表 (realtime_snapshot)
```sql
CREATE TABLE realtime_snapshot (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
security_type VARCHAR(20) NOT NULL, -- stock, future, index, etc.
trade_time TIMESTAMP NOT NULL,
pre_close DECIMAL(12, 4),
last DECIMAL(12, 4),
open DECIMAL(12, 4),
high DECIMAL(12, 4),
low DECIMAL(12, 4),
close DECIMAL(12, 4),
volume BIGINT,
amount DECIMAL(18, 4),
ask_price1 DECIMAL(12, 4),
ask_volume1 INTEGER,
bid_price1 DECIMAL(12, 4),
bid_volume1 INTEGER,
-- ... 其他快照字段
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL -- 7天后过期
);
CREATE INDEX idx_snapshot_code_time ON realtime_snapshot(code, trade_time);
CREATE INDEX idx_snapshot_expires ON realtime_snapshot(expires_at);
```
#### 4.2.8 缓存任务表 (cache_tasks)
```sql
CREATE TABLE cache_tasks (
id SERIAL PRIMARY KEY,
task_name VARCHAR(200) NOT NULL,
task_type VARCHAR(50) NOT NULL, -- 'detect_missing', 'cache_data'
security_type VARCHAR(20) NOT NULL, -- 'stock', 'future'
period_type VARCHAR(10), -- daily, min1, etc.
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status VARCHAR(20) DEFAULT 'pending', -- pending, running, completed, failed
progress DECIMAL(5, 2) DEFAULT 0,
total_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
error_count INTEGER DEFAULT 0,
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE
);
```
#### 4.2.9 缓存记录表 (cache_records)
```sql
CREATE TABLE cache_records (
id BIGSERIAL PRIMARY KEY,
task_id INTEGER REFERENCES cache_tasks(id),
code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
record_count INTEGER NOT NULL,
status VARCHAR(20) DEFAULT 'success',
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
#### 4.2.10 API测试日志表 (api_test_logs)
```sql
CREATE TABLE api_test_logs (
id BIGSERIAL PRIMARY KEY,
test_name VARCHAR(200) NOT NULL,
api_category VARCHAR(50) NOT NULL,
api_endpoint VARCHAR(200) NOT NULL,
request_method VARCHAR(10) NOT NULL,
request_params JSONB,
response_data JSONB,
status_code INTEGER,
execution_time_ms INTEGER,
is_success BOOLEAN DEFAULT FALSE,
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_test_logs_category ON api_test_logs(api_category);
CREATE INDEX idx_test_logs_created ON api_test_logs(created_at);
```
---
## 五、API接口设计
### 5.1 认证相关接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/v1/auth/login | 用户登录 |
| POST | /api/v1/auth/logout | 用户登出 |
| GET | /api/v1/auth/me | 获取当前用户信息 |
| POST | /api/v1/auth/refresh | 刷新Token |
### 5.2 配置管理接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/configs/sdk | 获取SDK配置列表 |
| GET | /api/v1/configs/sdk/{id} | 获取指定SDK配置 |
| POST | /api/v1/configs/sdk | 创建SDK配置 |
| PUT | /api/v1/configs/sdk/{id} | 更新SDK配置 |
| DELETE | /api/v1/configs/sdk/{id} | 删除SDK配置 |
| POST | /api/v1/configs/sdk/{id}/test | 测试SDK连接 |
| POST | /api/v1/configs/sdk/{id}/set-default | 设为默认配置 |
### 5.3 基础数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/base/codes | 获取代码列表 |
| GET | /api/v1/base/codes/{code}/info | 获取证券信息 |
| GET | /api/v1/base/calendar | 获取交易日历 |
| GET | /api/v1/base/calendar/trading-days | 获取指定区间交易日 |
### 5.4 历史行情数据接口
#### 股票历史数据
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/stock/kline | 获取股票K线数据(单个/批量) |
| GET | /api/v1/stock/kline/{code}/chart | 获取股票K线图数据 |
| GET | /api/v1/stock/snapshot | 获取股票历史快照数据 |
#### 期货历史数据
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/future/kline | 获取期货K线数据(单个/批量) |
| GET | /api/v1/future/kline/{code}/chart | 获取期货K线图数据 |
| GET | /api/v1/future/snapshot | 获取期货历史快照数据 |
### 5.5 实时数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/realtime/snapshot | 获取最新快照数据 |
| POST | /api/v1/realtime/subscribe | 开始实时数据订阅 |
| DELETE | /api/v1/realtime/subscribe | 停止实时数据订阅 |
| GET | /api/v1/realtime/subscribe/status | 获取订阅状态 |
| GET | /api/v1/realtime/snapshot/stream | WebSocket实时数据流 |
### 5.6 财务数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/finance/balance-sheet | 获取资产负债表 |
| GET | /api/v1/finance/cash-flow | 获取现金流量表 |
| GET | /api/v1/finance/income | 获取利润表 |
| GET | /api/v1/finance/profit-express | 获取业绩快报 |
| GET | /api/v1/finance/profit-notice | 获取业绩预告 |
### 5.7 股东股本数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/shareholder/top10 | 获取十大股东数据 |
| GET | /api/v1/shareholder/count | 获取股东户数 |
| GET | /api/v1/shareholder/equity | 获取股本结构 |
### 5.8 融资融券数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/margin/summary | 获取融资融券汇总 |
| GET | /api/v1/margin/detail | 获取融资融券明细 |
### 5.9 指数数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/index/constituents | 获取指数成分股 |
| GET | /api/v1/index/weights | 获取指数权重 |
### 5.10 ETF数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/etf/pcf | 获取ETF申赎数据 |
| GET | /api/v1/etf/share | 获取ETF份额数据 |
### 5.11 可转债数据接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/kzz/issuance | 获取可转债发行数据 |
### 5.12 数据缓存管理接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/v1/cache/detect-missing | 检测缺失数据 |
| POST | /api/v1/cache/batch-cache | 批量缓存数据 |
| GET | /api/v1/cache/tasks | 获取缓存任务列表 |
| GET | /api/v1/cache/tasks/{id} | 获取任务详情 |
| DELETE | /api/v1/cache/tasks/{id} | 取消任务 |
| GET | /api/v1/cache/status/{code} | 获取代码缓存状态 |
### 5.13 测试中心接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/test/categories | 获取测试分类 |
| GET | /api/v1/test/endpoints | 获取所有接口列表 |
| POST | /api/v1/test/run | 执行单个接口测试 |
| POST | /api/v1/test/run-all | 执行全部接口测试 |
| GET | /api/v1/test/history | 获取测试历史记录 |
| GET | /api/v1/test/history/{id} | 获取单次测试详情 |
---
## 六、核心业务逻辑
### 6.1 数据获取流程 (缓存优先)
```
用户请求 → 检查本地缓存 → 有数据 → 直接返回
无数据/不完整
调用AmazingData SDK → 获取数据 → 写入缓存 → 返回数据
```
### 6.2 缺失数据检测算法
```python
# 伪代码
def detect_missing_data(code, start_date, end_date, period='daily'):
# 1. 获取指定区间的交易日列表
trading_days = get_trading_days(start_date, end_date)
# 2. 查询数据库中已有数据的日期
existing_data = query_db_for_existing_dates(code, start_date, end_date, period)
# 3. 计算每个交易日的数据条数
missing_dates = []
for date in trading_days:
expected_count = get_expected_count_for_date(code, date, period)
actual_count = existing_data.get(date, 0)
# 4. 偏差超过10%认为缺失
if actual_count < expected_count * 0.9:
missing_dates.append({
'date': date,
'expected': expected_count,
'actual': actual_count,
'missing_ratio': 1 - actual_count / expected_count
})
return missing_dates
```
### 6.3 数据去重插入逻辑
```python
# 伪代码
def cache_data_with_dedup(code, data_df, period='daily'):
# 1. 构建唯一键
if period == 'daily':
existing = query_existing_data(code, data_df.index.min(), data_df.index.max())
else:
existing = query_existing_data(code, data_df.index.min(), data_df.index.max(), period)
# 2. 比较核心数据字段 (open, high, low, close, volume)
core_columns = ['open', 'high', 'low', 'close', 'volume']
to_insert = []
for idx, row in data_df.iterrows():
key = (code, idx) if period == 'daily' else (code, period, idx)
if key in existing:
# 3. 数据已存在,比较核心字段
existing_row = existing[key]
is_same = all(
abs(row[col] - existing_row[col]) < 0.0001
for col in core_columns
)
if is_same:
continue # 数据相同,跳过
to_insert.append(row)
# 4. 批量插入新数据
if to_insert:
batch_insert(to_insert)
```
### 6.4 实时数据订阅流程
```
前端请求订阅 → 后端建立WebSocket连接 → 初始化AmazingData订阅
前端 ← WebSocket推送 ← 回调函数接收数据 ← SDK实时推送
```
---
## 七、前端页面设计
### 7.1 页面结构
```
┌─────────────────────────────────────────────────────────────┐
│ [Logo] AmazingData金融数据服务平台 [用户] [退出] │
├─────────────────────────────────────────────────────────────┤
│ ┌──────┐ │
│ │ 首页 │ ┌──────────────────────────────────────────┐ │
│ ├──────┤ │ │ │
│ │数据查询│ │ 页面内容区域 │ │
│ ├──────┤ │ │ │
│ │配置管理│ │ │ │
│ ├──────┤ │ │ │
│ │缓存管理│ │ │ │
│ ├──────┤ │ │ │
│ │测试中心│ │ │ │
│ └──────┘ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 7.2 登录页面
- 用户名/密码输入框
- 记住我选项
- 登录按钮
### 7.3 配置管理页面
- SDK配置列表卡片形式
- 添加/编辑配置表单(弹窗)
- 测试连接按钮
- 设为默认按钮
### 7.4 数据查询页面
- **K线图查询**
- 代码输入(支持股票/期货)
- 时间区间选择默认1年
- 周期选择(日线/分钟线)
- K线图展示ECharts
- 数据表格展示
- **批量数据查询**
- 多代码输入(支持批量粘贴)
- 时间区间选择
- 数据列表展示(分页)
- 导出功能
### 7.5 缓存管理页面
- **缺失数据检测**
- 品种选择(股票/期货)
- 时间区间选择
- 检测结果列表
- 一键缓存按钮
- **缓存任务管理**
- 任务列表(进度条展示)
- 任务详情(成功/失败统计)
- 取消任务按钮
### 7.6 测试中心页面
- 接口分类树(基础数据/历史数据/实时数据/财务数据/...
- 接口列表(表格)
- 单个接口测试(参数表单 + 结果展示)
- 一键全部测试按钮
- 测试历史记录
---
## 八、项目目录结构
```
amazing-data-service/
├── backend/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py # FastAPI入口
│ │ ├── config.py # 配置管理
│ │ ├── dependencies.py # 依赖注入
│ │ ├── core/
│ │ │ ├── __init__.py
│ │ │ ├── security.py # JWT认证
│ │ │ ├── exceptions.py # 异常处理
│ │ │ └── middleware.py # 中间件
│ │ ├── api/
│ │ │ ├── __init__.py
│ │ │ ├── v1/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── auth.py # 认证接口
│ │ │ │ ├── configs.py # 配置接口
│ │ │ │ ├── base_data.py # 基础数据接口
│ │ │ │ ├── stock.py # 股票数据接口
│ │ │ │ ├── future.py # 期货数据接口
│ │ │ │ ├── realtime.py # 实时数据接口
│ │ │ │ ├── finance.py # 财务数据接口
│ │ │ │ ├── shareholder.py # 股东数据接口
│ │ │ │ ├── margin.py # 融资融券接口
│ │ │ │ ├── index.py # 指数数据接口
│ │ │ │ ├── etf.py # ETF数据接口
│ │ │ │ ├── kzz.py # 可转债数据接口
│ │ │ │ ├── cache.py # 缓存管理接口
│ │ │ │ └── test.py # 测试中心接口
│ │ ├── models/
│ │ │ ├── __init__.py
│ │ │ ├── user.py # 用户模型
│ │ │ ├── config.py # 配置模型
│ │ │ ├── stock.py # 股票数据模型
│ │ │ ├── future.py # 期货数据模型
│ │ │ ├── realtime.py # 实时数据模型
│ │ │ ├── finance.py # 财务数据模型
│ │ │ ├── cache.py # 缓存任务模型
│ │ │ └── test.py # 测试日志模型
│ │ ├── schemas/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py # 认证Schema
│ │ │ ├── base.py # 基础Schema
│ │ │ ├── config.py # 配置Schema
│ │ │ ├── kline.py # K线Schema
│ │ │ ├── finance.py # 财务Schema
│ │ │ ├── cache.py # 缓存Schema
│ │ │ └── test.py # 测试Schema
│ │ ├── services/
│ │ │ ├── __init__.py
│ │ │ ├── auth_service.py # 认证服务
│ │ │ ├── config_service.py # 配置服务
│ │ │ ├── amazing_data_service.py # SDK封装服务
│ │ │ ├── cache_service.py # 缓存服务
│ │ │ ├── stock_service.py # 股票数据服务
│ │ │ ├── future_service.py # 期货数据服务
│ │ │ ├── realtime_service.py # 实时数据服务
│ │ │ ├── finance_service.py # 财务数据服务
│ │ │ ├── base_data_service.py # 基础数据服务
│ │ │ └── test_service.py # 测试服务
│ │ ├── db/
│ │ │ ├── __init__.py
│ │ │ ├── base.py # 数据库基础
│ │ │ └── session.py # 会话管理
│ │ └── utils/
│ │ ├── __init__.py
│ │ ├── date_utils.py # 日期工具
│ │ ├── data_utils.py # 数据处理工具
│ │ └── validators.py # 验证器
│ ├── alembic/ # 数据库迁移
│ ├── tests/
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── api/ # API接口封装
│ │ ├── components/ # 公共组件
│ │ ├── views/ # 页面视图
│ │ │ ├── Login.vue
│ │ │ ├── Dashboard.vue
│ │ │ ├── ConfigManager.vue
│ │ │ ├── DataQuery/
│ │ │ │ ├── KlineChart.vue
│ │ │ │ └── BatchQuery.vue
│ │ │ ├── CacheManager/
│ │ │ │ ├── DetectMissing.vue
│ │ │ │ └── TaskManager.vue
│ │ │ └── TestCenter/
│ │ │ ├── ApiTest.vue
│ │ │ └── TestHistory.vue
│ │ ├── router/
│ │ ├── store/
│ │ ├── utils/
│ │ └── App.vue
│ ├── public/
│ ├── package.json
│ └── vite.config.ts
├── database/
│ ├── init.sql # 数据库初始化脚本
│ └── migrations/
├── docker/
│ ├── docker-compose.yml
│ └── nginx.conf
├── docs/
│ ├── api.md
│ └── deploy.md
├── scripts/
│ ├── init_db.py
│ └── create_admin.py
└── README.md
```
---
## 九、部署架构
```
┌─────────────────────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Nginx │ │ Frontend │ │ Backend │ │
│ │ (反向代理) │ │ (Vue3) │ │ (FastAPI) │ │
│ └──────┬───────┘ └──────────────┘ └──────┬───────┘ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ (AmazingData │ │
│ │ (主数据库) │ │ (缓存) │ │ SDK连接) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 十、关键功能实现要点
### 10.1 AmazingData SDK适配器
```python
class AmazingDataAdapter:
"""SDK适配器封装AmazingData所有接口"""
def __init__(self, config: SDKConfig):
self.config = config
self._ad = None
self._base_data = None
self._market_data = None
self._info_data = None
self._calendar = None
def connect(self) -> bool:
"""建立连接"""
import AmazingData as ad
ad.login(**self.config.to_dict())
self._ad = ad
self._base_data = ad.BaseData()
self._info_data = ad.InfoData()
self._calendar = self._base_data.get_calendar()
self._market_data = ad.MarketData(self._calendar)
return True
# 基础数据接口
def get_code_list(self, security_type: str) -> List[str]: ...
def get_code_info(self, security_type: str) -> pd.DataFrame: ...
def get_trading_calendar(self, market: str) -> List[int]: ...
# 历史行情接口
def get_kline(self, codes, start_date, end_date, period) -> Dict[str, pd.DataFrame]: ...
def get_snapshot(self, codes, start_date, end_date) -> Dict[str, pd.DataFrame]: ...
# 财务数据接口
def get_balance_sheet(self, codes, start_date, end_date) -> Dict[str, pd.DataFrame]: ...
def get_cash_flow(self, codes, start_date, end_date) -> Dict[str, pd.DataFrame]: ...
def get_income_statement(self, codes, start_date, end_date) -> Dict[str, pd.DataFrame]: ...
# 其他所有接口...
```
### 10.2 缓存服务
```python
class CacheService:
"""数据缓存服务"""
def get_kline_with_cache(self, code: str, start_date: date,
end_date: date, period: str) -> pd.DataFrame:
"""优先从缓存获取否则从SDK获取并缓存"""
# 1. 查询本地数据库
cached_data = self.db.query_kline(code, start_date, end_date, period)
# 2. 检查数据完整性
if self._is_data_complete(cached_data, start_date, end_date):
return cached_data
# 3. 从SDK获取缺失数据
missing_ranges = self._find_missing_ranges(cached_data, start_date, end_date)
for range_start, range_end in missing_ranges:
sdk_data = self.sdk.get_kline(code, range_start, range_end, period)
self.db.save_kline(code, sdk_data, period)
# 4. 重新查询完整数据
return self.db.query_kline(code, start_date, end_date, period)
```
### 10.3 实时数据WebSocket
```python
class RealtimeManager:
"""实时数据管理器"""
def __init__(self):
self.subscribers: Dict[str, Set[WebSocket]] = {}
self.sdk_subscriber = None
async def subscribe(self, websocket: WebSocket, codes: List[str]):
"""客户端订阅"""
await websocket.accept()
for code in codes:
if code not in self.subscribers:
self.subscribers[code] = set()
# 启动SDK订阅
self._start_sdk_subscription(code)
self.subscribers[code].add(websocket)
def _on_sdk_data(self, code: str, data: dict):
"""SDK数据回调"""
# 1. 保存到数据库(带过期时间)
self.db.save_realtime(data)
# 2. 推送给所有订阅者
for ws in self.subscribers.get(code, []):
asyncio.create_task(ws.send_json(data))
```
---
## 十一、开发计划
| 阶段 | 任务 | 预计时间 |
|------|------|----------|
| 第一阶段 | 项目初始化、数据库设计、基础框架搭建 | 2天 |
| 第二阶段 | 认证模块、配置管理模块开发 | 2天 |
| 第三阶段 | SDK适配器、基础数据接口开发 | 3天 |
| 第四阶段 | 历史数据接口(股票/期货)、缓存逻辑 | 4天 |
| 第五阶段 | 实时数据订阅功能 | 3天 |
| 第六阶段 | 财务/股东/融资融券等数据接口 | 3天 |
| 第七阶段 | 批量缓存管理功能 | 3天 |
| 第八阶段 | 测试中心功能 | 2天 |
| 第九阶段 | 前端页面开发 | 5天 |
| 第十阶段 | 集成测试、部署文档 | 2天 |
**总计约29天**
---
## 十二、风险与注意事项
1. **数据准确性**AmazingData SDK返回的数据需要验证建议增加数据校验逻辑
2. **并发性能**大量数据查询时需要控制并发避免SDK连接超时
3. **存储空间**:历史数据量巨大,需要定期归档或清理
4. **SDK连接**AmazingData SDK需要保持连接建议使用连接池
5. **实时数据延迟**:网络延迟可能导致实时数据推送不及时
---
*文档版本: 1.0*
*创建日期: 2025年*

@ -0,0 +1,198 @@
# AmazingData 金融数据服务平台
基于银河证券星耀数智量化平台SDK构建的金融数据服务系统提供历史数据缓存、实时数据订阅、批量数据管理等功能。
## 功能特性
- ✅ **完整SDK接口封装** - 封装AmazingData所有接口
- ✅ **智能数据缓存** - 优先本地缓存缺失自动从SDK补全
- ✅ **实时数据订阅** - WebSocket实时推送股票/期货行情
- ✅ **缺失数据检测** - 自动检测并提示缺失数据
- ✅ **批量缓存管理** - 一键检测、一键缓存
- ✅ **可视化K线图** - ECharts展示历史K线数据
- ✅ **完整测试中心** - 所有接口一键测试
- ✅ **配置管理** - SDK账号配置可视化维护
## 技术栈
| 层级 | 技术 |
|------|------|
| 后端 | Python 3.10+, FastAPI, SQLAlchemy 2.0 |
| 数据库 | PostgreSQL 15+ |
| 缓存 | Redis 7+ |
| 前端 | Vue3, TypeScript, Element Plus |
| 图表 | ECharts 5+ |
| 部署 | Docker, Docker Compose |
## 快速开始
### 1. 克隆项目
```bash
git clone <repository-url>
cd amazing-data-service
```
### 2. 安装AmazingData SDK
```bash
# 从银河证券获取 SDK wheel 文件
pip install AmazingData-1.0.24-py3-none-any.whl
```
### 3. 使用Docker启动
```bash
docker-compose up -d
```
服务将启动在:
- 前端: http://localhost
- 后端API: http://localhost:8000
- API文档: http://localhost:8000/docs
### 4. 初始化数据库
```bash
# 数据库会自动执行 init.sql 初始化脚本
# 默认管理员账号: admin / admin123
```
### 5. 配置SDK连接
1. 登录系统admin / admin123
2. 进入"配置管理"页面
3. 添加AmazingData SDK连接信息
4. 点击"测试连接"验证
## 项目结构
```
amazing-data-service/
├── backend/ # FastAPI后端
├── frontend/ # Vue3前端
├── database/ # 数据库脚本
├── docker/ # Docker配置
├── docs/ # 文档
└── README.md
```
## API接口文档
启动服务后访问http://localhost:8000/docs
### 主要接口分类
- `/api/v1/auth/*` - 认证接口
- `/api/v1/configs/*` - 配置管理
- `/api/v1/stock/*` - 股票数据
- `/api/v1/future/*` - 期货数据
- `/api/v1/realtime/*` - 实时数据
- `/api/v1/finance/*` - 财务数据
- `/api/v1/cache/*` - 缓存管理
- `/api/v1/test/*` - 测试中心
## 核心功能说明
### 1. 数据缓存策略
系统采用"本地优先"策略:
1. 查询请求首先检查本地数据库
2. 检查数据完整性(与交易日历对比)
3. 缺失数据自动从AmazingData SDK获取
4. 新数据写入数据库时自动去重
### 2. 缺失数据检测
检测逻辑:
- 按交易日查询数据条数
- 与最近交易日数据对比
- 偏差超过10%标记为缺失
### 3. 实时数据订阅
- WebSocket连接`ws://localhost:8000/api/v1/realtime/stream`
- 支持股票、期货、指数快照订阅
- 数据自动保存7天后清理
### 4. 测试中心
提供所有接口的自动化测试:
- 单个接口测试(参数调试)
- 批量接口测试(回归测试)
- 测试历史记录
## 数据库表结构
### 核心表
| 表名 | 说明 |
|------|------|
| users | 系统用户 |
| sdk_configs | SDK配置 |
| stock_kline_daily | 股票日线数据 |
| stock_kline_min | 股票分钟数据 |
| future_kline_daily | 期货日线数据 |
| future_kline_min | 期货分钟数据 |
| realtime_snapshot | 实时快照数据 |
| cache_tasks | 缓存任务 |
| api_test_logs | API测试日志 |
完整表结构见:`database/init.sql`
## 开发指南
### 后端开发
```bash
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload
```
### 前端开发
```bash
cd frontend
npm install
npm run dev
```
## 配置说明
### 环境变量
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| DATABASE_URL | 数据库连接URL | postgresql://postgres:postgres@localhost:5432/amazing_data |
| REDIS_URL | Redis连接URL | redis://localhost:6379/0 |
| SECRET_KEY | JWT密钥 | your-secret-key |
| ACCESS_TOKEN_EXPIRE_HOURS | Token过期时间 | 24 |
## 常见问题
### Q: 如何添加新的SDK配置
A: 登录系统后,进入"配置管理"页面,点击"添加配置"填写AmazingData账号信息。
### Q: 如何批量缓存历史数据?
A: 进入"缓存管理"页面,选择时间区间,点击"检测缺失数据",然后点击"一键缓存"。
### Q: 实时数据如何订阅?
A: 在数据查询页面,输入代码后勾选"实时订阅"系统会自动建立WebSocket连接。
### Q: 数据去重是如何工作的?
A: 系统比较open/high/low/close/volume五个核心字段如果差异小于0.0001则视为相同数据,跳过插入。
## 许可证
MIT License
## 联系支持
如有问题请联系项目维护者或提交Issue。
---
**注意**使用本系统需要AmazingData SDK授权请联系银河证券开通权限。

@ -0,0 +1,14 @@
# 数据库配置
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/amazing_data
# Redis配置
REDIS_URL=redis://localhost:6379/0
# JWT密钥生产环境请修改
SECRET_KEY=your-secret-key-change-in-production
# Token过期时间小时
ACCESS_TOKEN_EXPIRE_HOURS=24
# 调试模式
DEBUG=false

@ -0,0 +1,26 @@
# 后端Dockerfile
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 复制依赖文件
COPY requirements.txt .
# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY app/ ./app/
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

@ -0,0 +1 @@
# AmazingData金融数据服务平台 - 后端应用

@ -0,0 +1,16 @@
# API v1模块
from fastapi import APIRouter
from app.api.v1 import auth, configs, base_data, stock, future, realtime, finance, cache, test
api_router = APIRouter(prefix="/api/v1")
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
api_router.include_router(configs.router, prefix="/configs", tags=["配置管理"])
api_router.include_router(base_data.router, prefix="/base", tags=["基础数据"])
api_router.include_router(stock.router, prefix="/stock", tags=["股票数据"])
api_router.include_router(future.router, prefix="/future", tags=["期货数据"])
api_router.include_router(realtime.router, prefix="/realtime", tags=["实时数据"])
api_router.include_router(finance.router, prefix="/finance", tags=["财务数据"])
api_router.include_router(cache.router, prefix="/cache", tags=["缓存管理"])
api_router.include_router(test.router, prefix="/test", tags=["测试中心"])

@ -0,0 +1,42 @@
"""
认证路由
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.auth import UserLogin, TokenResponse, UserInfo
from app.schemas.base import ResponseModel
from app.services.auth_service import AuthService
from app.core.security import get_current_user
from app.models.user import User
router = APIRouter()
security = HTTPBearer()
@router.post("/login", response_model=ResponseModel[TokenResponse])
async def login(login_data: UserLogin, db: Session = Depends(get_db)):
"""
用户登录
- **username**: 用户名
- **password**: 密码
"""
user = AuthService.authenticate_user(db, login_data.username, login_data.password)
token_data = AuthService.create_user_token(user)
return ResponseModel(data=TokenResponse(**token_data))
@router.get("/me", response_model=ResponseModel[UserInfo])
async def get_me(current_user: User = Depends(get_current_user)):
"""获取当前用户信息"""
return ResponseModel(data=UserInfo.model_validate(current_user))
@router.post("/logout", response_model=ResponseModel)
async def logout():
"""用户登出前端清除token即可"""
return ResponseModel(message="登出成功")

@ -0,0 +1,101 @@
"""
基础数据路由
"""
from typing import List
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.services.base_data_service import BaseDataService
from app.core.security import get_current_user
from app.models.user import User
from app.utils.date_utils import parse_date
router = APIRouter()
@router.get("/codes", response_model=ResponseModel)
async def get_code_list(
security_type: str = Query(..., description="证券类型: EXTRA_STOCK_A, EXTRA_FUTURE, EXTRA_ETF, EXTRA_INDEX_A"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取代码列表"""
service = BaseDataService(db)
codes = service.get_code_list(security_type)
return ResponseModel(data={"codes": codes[:100]}) # 限制返回数量
@router.get("/codes/{code}/info", response_model=ResponseModel)
async def get_code_info(
code: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取证券信息"""
service = BaseDataService(db)
# 根据代码判断证券类型
security_type = service.get_security_type(code)
# 获取对应类型的代码信息
if security_type == "stock":
info = service.get_code_info("EXTRA_STOCK_A")
elif security_type == "future":
info = service.get_code_info("EXTRA_FUTURE")
else:
info = None
if info is not None and not info.empty:
code_info = info[info.get("code") == code]
if not code_info.empty:
return ResponseModel(data=code_info.to_dict("records")[0])
return ResponseModel(data={"code": code, "security_type": security_type})
@router.get("/calendar", response_model=ResponseModel)
async def get_trading_calendar(
market: str = Query("SH", description="市场: SH, SZ, CFE"),
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取交易日历"""
service = BaseDataService(db)
start = parse_date(start_date)
end = parse_date(end_date)
calendar = service.get_trading_calendar(market, start, end)
return ResponseModel(data={
"market": market,
"start_date": start_date,
"end_date": end_date,
"trading_days": [d.isoformat() for d in calendar],
"count": len(calendar)
})
@router.get("/calendar/trading-days", response_model=ResponseModel)
async def get_trading_days(
market: str = Query("SH", description="市场: SH, SZ, CFE"),
days: int = Query(30, description="最近交易日数量"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取最近交易日列表"""
from datetime import date, timedelta
service = BaseDataService(db)
end_date = date.today()
start_date = end_date - timedelta(days=days * 2) # 获取更多天数以确保有足够交易日
calendar = service.get_trading_calendar(market, start_date, end_date)
recent_days = calendar[-days:] if len(calendar) > days else calendar
return ResponseModel(data={
"market": market,
"trading_days": [d.isoformat() for d in recent_days],
"count": len(recent_days)
})

@ -0,0 +1,169 @@
"""
缓存管理路由
"""
from typing import List
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.base import ResponseModel, PaginatedResponse
from app.schemas.cache import (
DetectMissingRequest, DetectMissingResponse,
BatchCacheRequest, CacheTaskResponse, CacheStatusResponse
)
from app.services.cache_service import CacheService
from app.core.security import get_current_user
from app.models.user import User
from app.utils.date_utils import parse_date
router = APIRouter()
@router.post("/detect-missing", response_model=ResponseModel)
async def detect_missing_data(
request: DetectMissingRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""检测缺失数据"""
service = CacheService(db)
start = parse_date(request.start_date)
end = parse_date(request.end_date)
task = service.detect_missing_data(
request.security_type,
request.period_type,
start,
end,
request.code_list
)
# 获取缺失详情
details = service.get_task_details(task.id)
missing_codes = [d for d in details if d.is_missing]
missing_info = []
for code in request.code_list:
code_details = [d for d in details if d.code == code and d.is_missing]
if code_details:
missing_info.append({
"code": code,
"missing_dates": [{
"date": format_date(d.trade_date),
"expected": d.expected_count,
"actual": d.actual_count,
"missing_ratio": (d.expected_count - d.actual_count) / d.expected_count if d.expected_count > 0 else 0
} for d in code_details]
})
return ResponseModel(data={
"task_id": task.id,
"total_codes": len(request.code_list),
"missing_codes": missing_info
})
@router.post("/batch-cache", response_model=ResponseModel[CacheTaskResponse])
async def batch_cache_data(
request: BatchCacheRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""批量缓存数据"""
service = CacheService(db)
start = parse_date(request.start_date)
end = parse_date(request.end_date)
task = service.batch_cache_data(
request.security_type,
request.period_type,
start,
end,
request.code_list
)
return ResponseModel(data=CacheTaskResponse.model_validate(task))
@router.get("/tasks", response_model=ResponseModel)
async def get_cache_tasks(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取缓存任务列表"""
service = CacheService(db)
result = service.get_tasks(page, page_size)
return ResponseModel(data={
"items": [CacheTaskResponse.model_validate(t) for t in result["items"]],
"total": result["total"],
"page": result["page"],
"page_size": result["page_size"],
"total_pages": result["total_pages"]
})
@router.get("/tasks/{task_id}", response_model=ResponseModel)
async def get_cache_task(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取缓存任务详情"""
service = CacheService(db)
task = service.get_task(task_id)
if not task:
return ResponseModel(code=404, message="任务不存在")
details = service.get_task_details(task_id)
return ResponseModel(data={
"task": CacheTaskResponse.model_validate(task),
"details": [{
"id": d.id,
"code": d.code,
"trade_date": d.trade_date.isoformat() if d.trade_date else None,
"expected_count": d.expected_count,
"actual_count": d.actual_count,
"is_missing": bool(d.is_missing),
"status": d.status,
"error_message": d.error_message
} for d in details]
})
@router.delete("/tasks/{task_id}", response_model=ResponseModel)
async def cancel_cache_task(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""取消缓存任务"""
service = CacheService(db)
success = service.cancel_task(task_id)
if success:
return ResponseModel(message="任务已取消")
else:
return ResponseModel(code=400, message="任务不存在或已完成")
@router.get("/status/{code}", response_model=ResponseModel)
async def get_cache_status(
code: str,
security_type: str = Query("stock", description="证券类型: stock, future"),
period_type: str = Query("daily", description="周期类型: daily, min1, min5, etc."),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取代码缓存状态"""
service = CacheService(db)
status = service.get_cache_status(code, security_type, period_type)
return ResponseModel(data=status)
from app.utils.date_utils import format_date

@ -0,0 +1,158 @@
"""
配置管理路由
"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.config import SDKConfigCreate, SDKConfigUpdate, SDKConfigResponse, SDKConfigTestResponse
from app.schemas.base import ResponseModel, PaginatedResponse, PaginatedData
from app.services.config_service import ConfigService
from app.services.amazing_data_adapter import AmazingDataAdapter
from app.services.sdk_manager import sdk_manager
from app.core.security import get_current_user
from app.models.user import User
router = APIRouter()
@router.get("/sdk", response_model=ResponseModel[List[SDKConfigResponse]])
async def get_sdk_configs(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取SDK配置列表"""
configs = ConfigService.get_sdk_configs(db)
return ResponseModel(data=[SDKConfigResponse.model_validate(c) for c in configs])
@router.get("/sdk/{config_id}", response_model=ResponseModel[SDKConfigResponse])
async def get_sdk_config(
config_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取指定SDK配置"""
config = ConfigService.get_sdk_config(db, config_id)
if not config:
raise HTTPException(status_code=404, detail="配置不存在")
return ResponseModel(data=SDKConfigResponse.model_validate(config))
@router.post("/sdk", response_model=ResponseModel[SDKConfigResponse])
async def create_sdk_config(
config_data: SDKConfigCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""创建SDK配置"""
config_dict = config_data.model_dump()
config = ConfigService.create_sdk_config(db, config_dict)
return ResponseModel(data=SDKConfigResponse.model_validate(config))
@router.put("/sdk/{config_id}", response_model=ResponseModel[SDKConfigResponse])
async def update_sdk_config(
config_id: int,
config_data: SDKConfigUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""更新SDK配置"""
config_dict = config_data.model_dump(exclude_unset=True)
config = ConfigService.update_sdk_config(db, config_id, config_dict)
return ResponseModel(data=SDKConfigResponse.model_validate(config))
@router.delete("/sdk/{config_id}", response_model=ResponseModel)
async def delete_sdk_config(
config_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""删除SDK配置"""
ConfigService.delete_sdk_config(db, config_id)
return ResponseModel(message="删除成功")
@router.post("/sdk/{config_id}/test", response_model=ResponseModel[SDKConfigTestResponse])
async def test_sdk_config(
config_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""测试SDK连接连接成功后会保持登录状态"""
import asyncio
config = ConfigService.get_sdk_config(db, config_id)
if not config:
raise HTTPException(status_code=404, detail="配置不存在")
def _test_connection():
adapter = sdk_manager.get_connection(config_id)
if adapter:
return True
return False
success = await asyncio.to_thread(_test_connection)
if success:
return ResponseModel(data=SDKConfigTestResponse(success=True, message="连接成功(已保持登录状态)"))
else:
return ResponseModel(
code=1001,
message="连接失败",
data=SDKConfigTestResponse(success=False, message="SDK连接失败请检查配置")
)
@router.post("/sdk/{config_id}/release", response_model=ResponseModel)
async def release_sdk_connection(
config_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""释放SDK连接登出释放资源"""
import asyncio
config = ConfigService.get_sdk_config(db, config_id)
if not config:
raise HTTPException(status_code=404, detail="配置不存在")
def _release():
sdk_manager.release_connection(config_id)
await asyncio.to_thread(_release)
return ResponseModel(message="SDK连接已释放")
@router.get("/sdk/{config_id}/status", response_model=ResponseModel)
async def get_sdk_connection_status(
config_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取SDK连接状态"""
config = ConfigService.get_sdk_config(db, config_id)
if not config:
raise HTTPException(status_code=404, detail="配置不存在")
status_info = sdk_manager.get_status(config_id)
return ResponseModel(data={
"connected": status_info["connected"],
"last_activity": status_info["last_activity"],
"config_id": status_info["config_id"]
})
@router.post("/sdk/{config_id}/set-default", response_model=ResponseModel)
async def set_default_config(
config_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""设为默认配置"""
ConfigService.set_default_config(db, config_id)
return ResponseModel(message="设置成功")

@ -0,0 +1,113 @@
"""
财务数据路由
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.services.finance_service import FinanceService
from app.core.security import get_current_user
from app.models.user import User
from app.utils.date_utils import parse_date
router = APIRouter()
@router.get("/balance-sheet", response_model=ResponseModel)
async def get_balance_sheet(
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取资产负债表"""
service = FinanceService(db)
code_list = [c.strip() for c in codes.split(",")]
start = parse_date(start_date)
end = parse_date(end_date)
data = service.get_balance_sheet(code_list, start, end)
return ResponseModel(data=data)
@router.get("/cash-flow", response_model=ResponseModel)
async def get_cash_flow(
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取现金流量表"""
service = FinanceService(db)
code_list = [c.strip() for c in codes.split(",")]
start = parse_date(start_date)
end = parse_date(end_date)
data = service.get_cash_flow(code_list, start, end)
return ResponseModel(data=data)
@router.get("/income", response_model=ResponseModel)
async def get_income(
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取利润表"""
service = FinanceService(db)
code_list = [c.strip() for c in codes.split(",")]
start = parse_date(start_date)
end = parse_date(end_date)
data = service.get_income_statement(code_list, start, end)
return ResponseModel(data=data)
@router.get("/profit-express", response_model=ResponseModel)
async def get_profit_express(
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取业绩快报"""
service = FinanceService(db)
code_list = [c.strip() for c in codes.split(",")]
start = parse_date(start_date)
end = parse_date(end_date)
# 从SDK获取业绩快报
try:
adapter = service.base_service._get_adapter()
data = adapter.get_profit_express(code_list, start, end)
return ResponseModel(data=data.to_dict("records") if not data.empty else [])
except Exception as e:
return ResponseModel(data=[], message=str(e))
@router.get("/profit-notice", response_model=ResponseModel)
async def get_profit_notice(
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始报告期(YYYYMMDD)"),
end_date: str = Query(..., description="结束报告期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取业绩预告"""
service = FinanceService(db)
code_list = [c.strip() for c in codes.split(",")]
start = parse_date(start_date)
end = parse_date(end_date)
try:
adapter = service.base_service._get_adapter()
data = adapter.get_profit_notice(code_list, start, end)
return ResponseModel(data=data.to_dict("records") if not data.empty else [])
except Exception as e:
return ResponseModel(data=[], message=str(e))

@ -0,0 +1,79 @@
"""
期货数据路由
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.schemas.kline import BatchKlineRequest
from app.services.future_service import FutureService
from app.core.security import get_current_user
from app.models.user import User
from app.utils.date_utils import parse_date
router = APIRouter()
@router.get("/kline", response_model=ResponseModel)
async def get_future_kline(
codes: str = Query(..., description="期货代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
period: str = Query("daily", description="周期: daily, min1, min5, min15, min30, min60"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取期货K线数据"""
service = FutureService(db)
code_list = [c.strip() for c in codes.split(",")]
start = parse_date(start_date)
end = parse_date(end_date)
data = service.get_kline(code_list, start, end, period)
return ResponseModel(data=data)
@router.post("/kline/batch", response_model=ResponseModel)
async def batch_get_future_kline(
request: BatchKlineRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""批量获取期货K线数据"""
service = FutureService(db)
start = parse_date(request.start_date)
end = parse_date(request.end_date)
data = service.get_kline(request.codes, start, end, request.period)
return ResponseModel(data=data)
@router.get("/kline/{code}/chart", response_model=ResponseModel)
async def get_future_kline_chart(
code: str,
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
period: str = Query("daily", description="周期: daily, min1, min5, min15, min30, min60"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取期货K线图数据ECharts格式"""
service = FutureService(db)
start = parse_date(start_date)
end = parse_date(end_date)
data = service.get_kline_chart_data(code, start, end, period)
return ResponseModel(data=data)
@router.get("/snapshot", response_model=ResponseModel)
async def get_future_snapshot(
codes: str = Query(..., description="期货代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取期货历史快照数据"""
return ResponseModel(data={"message": "功能开发中"})

@ -0,0 +1,109 @@
"""
实时数据路由
"""
from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.services.realtime_service import RealtimeService
from app.core.security import get_current_user
from app.models.user import User
router = APIRouter()
@router.get("/snapshot", response_model=ResponseModel)
async def get_realtime_snapshot(
codes: str = Query(..., description="代码列表,多个用逗号分隔"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取最新快照数据"""
service = RealtimeService(db)
code_list = [c.strip() for c in codes.split(",")]
data = service.get_latest_snapshot(code_list)
return ResponseModel(data=data)
@router.post("/subscribe", response_model=ResponseModel)
async def subscribe_realtime(
codes: str = Query(..., description="代码列表,多个用逗号分隔"),
types: str = Query("snapshot", description="订阅类型: snapshot, kline"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""开始实时数据订阅"""
code_list = [c.strip() for c in codes.split(",")]
return ResponseModel(data={
"message": "订阅成功",
"codes": code_list,
"types": types.split(",")
})
@router.delete("/subscribe", response_model=ResponseModel)
async def unsubscribe_realtime(
codes: str = Query(None, description="代码列表,多个用逗号分隔,为空则取消所有"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""停止实时数据订阅"""
code_list = [c.strip() for c in codes.split(",")] if codes else None
return ResponseModel(data={
"message": "取消订阅成功",
"codes": code_list
})
@router.get("/subscribe/status", response_model=ResponseModel)
async def get_subscribe_status(
current_user: User = Depends(get_current_user)
):
"""获取订阅状态"""
return ResponseModel(data={
"subscribed_codes": [],
"subscribed_types": []
})
@router.websocket("/stream")
async def realtime_websocket(websocket: WebSocket):
"""WebSocket实时数据流"""
# 获取查询参数
query_params = websocket.query_params
codes_str = query_params.get("codes", "")
types_str = query_params.get("types", "snapshot")
codes = [c.strip() for c in codes_str.split(",") if c.strip()]
if not codes:
await websocket.close(code=4000, reason="Missing codes parameter")
return
# 创建数据库会话
from app.db.session import SessionLocal
db = SessionLocal()
try:
service = RealtimeService(db)
await service.subscribe_websocket(websocket, codes)
# 保持连接
while True:
try:
# 接收客户端消息(心跳检测)
data = await websocket.receive_text()
# 回复心跳
await websocket.send_json({"type": "heartbeat", "timestamp": datetime.utcnow().isoformat()})
except WebSocketDisconnect:
break
except Exception as e:
break
finally:
await service.unsubscribe_websocket(websocket, codes)
db.close()
from datetime import datetime

@ -0,0 +1,81 @@
"""
股票数据路由
"""
from typing import List
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.schemas.kline import KlineRequest, BatchKlineRequest
from app.services.stock_service import StockService
from app.core.security import get_current_user
from app.models.user import User
from app.utils.date_utils import parse_date
router = APIRouter()
@router.get("/kline", response_model=ResponseModel)
async def get_stock_kline(
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
period: str = Query("daily", description="周期: daily, min1, min5, min15, min30, min60"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取股票K线数据"""
service = StockService(db)
code_list = [c.strip() for c in codes.split(",")]
start = parse_date(start_date)
end = parse_date(end_date)
data = service.get_kline(code_list, start, end, period)
return ResponseModel(data=data)
@router.post("/kline/batch", response_model=ResponseModel)
async def batch_get_stock_kline(
request: BatchKlineRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""批量获取股票K线数据"""
service = StockService(db)
start = parse_date(request.start_date)
end = parse_date(request.end_date)
data = service.get_kline(request.codes, start, end, request.period)
return ResponseModel(data=data)
@router.get("/kline/{code}/chart", response_model=ResponseModel)
async def get_stock_kline_chart(
code: str,
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
period: str = Query("daily", description="周期: daily, min1, min5, min15, min30, min60"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取股票K线图数据ECharts格式"""
service = StockService(db)
start = parse_date(start_date)
end = parse_date(end_date)
data = service.get_kline_chart_data(code, start, end, period)
return ResponseModel(data=data)
@router.get("/snapshot", response_model=ResponseModel)
async def get_stock_snapshot(
codes: str = Query(..., description="股票代码,多个用逗号分隔"),
start_date: str = Query(..., description="开始日期(YYYYMMDD)"),
end_date: str = Query(..., description="结束日期(YYYYMMDD)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取股票历史快照数据"""
# 这里可以实现快照数据查询
return ResponseModel(data={"message": "功能开发中"})

@ -0,0 +1,123 @@
"""
测试中心路由
"""
from typing import List
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.schemas.base import ResponseModel
from app.schemas.test import TestRequest, RunAllTestsRequest
from app.services.test_service import TestService
from app.core.security import get_current_user
from app.models.user import User
router = APIRouter()
@router.get("/categories", response_model=ResponseModel)
async def get_test_categories(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取测试分类"""
service = TestService(db)
categories = service.get_categories()
return ResponseModel(data=categories)
@router.get("/endpoints", response_model=ResponseModel)
async def get_test_endpoints(
category: str = Query(None, description="分类筛选"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取测试端点列表"""
service = TestService(db)
endpoints = service.get_endpoints(category)
return ResponseModel(data=endpoints)
@router.post("/run", response_model=ResponseModel)
async def run_single_test(
request: TestRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""执行单个接口测试"""
service = TestService(db)
result = service.run_test(request.endpoint, request.method, request.params or {})
return ResponseModel(data=result)
@router.post("/run-all", response_model=ResponseModel)
async def run_all_tests(
request: RunAllTestsRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""执行全部接口测试"""
service = TestService(db)
result = service.run_all_tests(request.categories)
return ResponseModel(data=result)
@router.get("/history", response_model=ResponseModel)
async def get_test_history(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取测试历史记录"""
service = TestService(db)
result = service.get_test_history(page, page_size)
return ResponseModel(data={
"items": [{
"id": log.id,
"test_name": log.test_name,
"api_category": log.api_category,
"api_endpoint": log.api_endpoint,
"request_method": log.request_method,
"status_code": log.status_code,
"execution_time_ms": log.execution_time_ms,
"is_success": log.is_success,
"error_message": log.error_message,
"created_at": log.created_at.isoformat() if log.created_at else None
} for log in result["items"]],
"total": result["total"],
"page": result["page"],
"page_size": result["page_size"],
"total_pages": result["total_pages"]
})
@router.get("/history/{test_id}", response_model=ResponseModel)
async def get_test_detail(
test_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取单次测试详情"""
from app.models.test import APITestLog
log = db.query(APITestLog).filter(APITestLog.id == test_id).first()
if not log:
return ResponseModel(code=404, message="测试记录不存在")
return ResponseModel(data={
"id": log.id,
"test_name": log.test_name,
"api_category": log.api_category,
"api_endpoint": log.api_endpoint,
"request_method": log.request_method,
"request_params": log.request_params,
"response_data": log.response_data,
"status_code": log.status_code,
"execution_time_ms": log.execution_time_ms,
"is_success": log.is_success,
"error_message": log.error_message,
"created_at": log.created_at.isoformat() if log.created_at else None
})

@ -0,0 +1,54 @@
"""
应用配置模块
"""
import os
from typing import Optional
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
"""应用配置类"""
# 应用配置
APP_NAME: str = "AmazingData金融数据服务平台"
APP_VERSION: str = "1.0.0"
DEBUG: bool = Field(default=True, env="DEBUG")
# 数据库配置 - 使用SQLite简化演示
DATABASE_URL: str = Field(
default="sqlite:///./amazing_data.db",
env="DATABASE_URL"
)
# Redis配置
REDIS_URL: str = Field(
default="redis://localhost:6379/0",
env="REDIS_URL"
)
# JWT配置
SECRET_KEY: str = Field(
default="your-secret-key-change-in-production",
env="SECRET_KEY"
)
ACCESS_TOKEN_EXPIRE_HOURS: int = Field(default=24, env="ACCESS_TOKEN_EXPIRE_HOURS")
ALGORITHM: str = "HS256"
# 缓存配置
CACHE_DEFAULT_PERIOD: str = "daily"
CACHE_DEFAULT_DAYS: int = 365
CACHE_AUTO_CLEANUP_DAYS: int = 7
CACHE_BATCH_SIZE: int = 100
CACHE_MISSING_THRESHOLD: float = 0.1
# 实时数据配置
REALTIME_SUBSCRIBE_INTERVAL: int = 1000
class Config:
env_file = ".env"
case_sensitive = True
# 全局配置实例
settings = Settings()

@ -0,0 +1,16 @@
# 核心模块
from app.core.security import (
verify_password,
get_password_hash,
create_access_token,
decode_token,
get_current_user
)
__all__ = [
"verify_password",
"get_password_hash",
"create_access_token",
"decode_token",
"get_current_user",
]

@ -0,0 +1,82 @@
"""
异常处理模块
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from sqlalchemy.exc import SQLAlchemyError
class BusinessException(Exception):
"""业务异常"""
def __init__(self, code: int, message: str):
self.code = code
self.message = message
super().__init__(message)
class SDKException(Exception):
"""SDK异常"""
def __init__(self, message: str, original_error: Exception = None):
self.message = message
self.original_error = original_error
super().__init__(message)
async def business_exception_handler(request: Request, exc: BusinessException):
"""业务异常处理器"""
return JSONResponse(
status_code=200,
content={
"code": exc.code,
"message": exc.message,
"data": None,
"timestamp": datetime.utcnow().isoformat()
}
)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""参数验证异常处理器"""
errors = []
for error in exc.errors():
errors.append(f"{'.'.join(str(x) for x in error['loc'])}: {error['msg']}")
return JSONResponse(
status_code=400,
content={
"code": 400,
"message": f"参数错误: {'; '.join(errors)}",
"data": None,
"timestamp": datetime.utcnow().isoformat()
}
)
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
"""数据库异常处理器"""
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": f"数据库错误: {str(exc)}",
"data": None,
"timestamp": datetime.utcnow().isoformat()
}
)
async def general_exception_handler(request: Request, exc: Exception):
"""通用异常处理器"""
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": f"服务器内部错误: {str(exc)}",
"data": None,
"timestamp": datetime.utcnow().isoformat()
}
)
from datetime import datetime

@ -0,0 +1,53 @@
"""
中间件模块
"""
import time
import logging
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.cors import CORSMiddleware
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class LoggingMiddleware(BaseHTTPMiddleware):
"""日志中间件"""
async def dispatch(self, request: Request, call_next):
start_time = time.time()
# 记录请求信息
logger.info(f"Request: {request.method} {request.url.path}")
response = await call_next(request)
# 计算处理时间
process_time = time.time() - start_time
# 记录响应信息
logger.info(
f"Response: {request.method} {request.url.path} "
f"- Status: {response.status_code} - Time: {process_time:.3f}s"
)
response.headers["X-Process-Time"] = str(process_time)
return response
def setup_cors(app):
"""配置CORS"""
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境应该限制具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def setup_middleware(app):
"""设置所有中间件"""
setup_cors(app)
app.add_middleware(LoggingMiddleware)

@ -0,0 +1,92 @@
"""
安全模块 - JWT认证和密码处理
"""
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
import bcrypt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.config import settings
from app.db.session import get_db
from app.models.user import User
security = HTTPBearer()
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
def get_password_hash(password: str) -> str:
"""获取密码哈希"""
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""创建JWT访问令牌"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def decode_token(token: str) -> Optional[dict]:
"""解码JWT令牌"""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> User:
"""获取当前用户"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
token = credentials.credentials
payload = decode_token(token)
if payload is None:
raise credentials_exception
username: str = payload.get("sub")
if username is None:
raise credentials_exception
user = db.query(User).filter(User.username == username).first()
if user is None:
raise credentials_exception
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="用户已被禁用"
)
return user
async def get_current_active_superuser(current_user: User = Depends(get_current_user)) -> User:
"""获取当前超级用户"""
if not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要超级用户权限"
)
return current_user

@ -0,0 +1,5 @@
# 数据库模块
from app.db.base import Base
from app.db.session import get_db, engine, SessionLocal
__all__ = ["Base", "get_db", "engine", "SessionLocal"]

@ -0,0 +1,9 @@
"""
SQLAlchemy基础模型
"""
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
"""基础模型类"""
pass

@ -0,0 +1,68 @@
"""
数据库会话管理
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from typing import Generator
from app.config import settings
from app.db.base import Base
# 创建数据库引擎
if settings.DATABASE_URL.startswith("sqlite"):
engine = create_engine(
settings.DATABASE_URL,
connect_args={"check_same_thread": False},
echo=settings.DEBUG
)
else:
engine = create_engine(
settings.DATABASE_URL,
pool_pre_ping=True,
pool_size=10,
max_overflow=20,
echo=settings.DEBUG
)
# 创建会话工厂
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
def get_db() -> Generator[Session, None, None]:
"""获取数据库会话的依赖函数"""
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db() -> None:
"""初始化数据库表"""
from app.models import user, config, stock, future, realtime, finance, cache, test
from app.models.user import User
from app.core.security import get_password_hash
Base.metadata.create_all(bind=engine)
db = SessionLocal()
try:
existing_admin = db.query(User).filter(User.username == "admin").first()
if not existing_admin:
admin_user = User(
username="admin",
password_hash=get_password_hash("admin123"),
is_active=True,
is_superuser=True
)
db.add(admin_user)
db.commit()
print("Default admin user created (admin/admin123)")
except Exception as e:
print(f"Error creating default user: {e}")
db.rollback()
finally:
db.close()

@ -0,0 +1,97 @@
"""
FastAPI主入口
"""
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
import os
from app.config import settings
from app.api.v1 import api_router
from app.core.middleware import setup_middleware
from app.core.exceptions import (
business_exception_handler,
validation_exception_handler,
sqlalchemy_exception_handler,
general_exception_handler,
BusinessException
)
from app.db.session import init_db
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时执行
print(f"Starting {settings.APP_NAME}...")
# 初始化数据库
try:
init_db()
print("Database initialized successfully")
except Exception as e:
print(f"Database initialization warning: {e}")
yield
# 关闭时执行
print(f"Shutting down {settings.APP_NAME}...")
# 创建FastAPI应用
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="AmazingData金融数据服务平台API",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan
)
# 设置中间件
setup_middleware(app)
# 注册异常处理器
app.add_exception_handler(BusinessException, business_exception_handler)
# 注册路由
app.include_router(api_router)
# 挂载静态文件
static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.exists(static_dir):
app.mount("/static", StaticFiles(directory=static_dir), name="static")
@app.get("/", tags=["根路径"])
async def root():
"""根路径 - 返回前端页面"""
index_file = os.path.join(static_dir, "index.html")
if os.path.exists(index_file):
return FileResponse(index_file)
return {
"name": settings.APP_NAME,
"version": settings.APP_VERSION,
"docs": "/docs",
"api": "/api/v1"
}
@app.get("/health", tags=["健康检查"])
async def health_check():
"""健康检查"""
return {
"status": "healthy",
"version": settings.APP_VERSION
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=settings.DEBUG
)

@ -0,0 +1,28 @@
# 模型模块
from app.models.user import User
from app.models.config import SDKConfig, SystemConfig
from app.models.stock import StockInfo, StockKlineDaily, StockKlineMin
from app.models.future import FutureInfo, FutureKlineDaily, FutureKlineMin
from app.models.realtime import RealtimeSnapshot
from app.models.finance import FinanceBalanceSheet, FinanceCashFlow, FinanceIncome
from app.models.cache import CacheTask, CacheTaskDetail
from app.models.test import APITestLog
__all__ = [
"User",
"SDKConfig",
"SystemConfig",
"StockInfo",
"StockKlineDaily",
"StockKlineMin",
"FutureInfo",
"FutureKlineDaily",
"FutureKlineMin",
"RealtimeSnapshot",
"FinanceBalanceSheet",
"FinanceCashFlow",
"FinanceIncome",
"CacheTask",
"CacheTaskDetail",
"APITestLog",
]

@ -0,0 +1,52 @@
"""
缓存任务模型
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, BigInteger, String, Numeric, Text, Date, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from app.db.base import Base
class CacheTask(Base):
"""缓存任务表"""
__tablename__ = "cache_tasks"
id = Column(Integer, primary_key=True, index=True)
task_name = Column(String(200), nullable=False)
task_type = Column(String(50), nullable=False) # detect_missing, cache_data, sync_data
security_type = Column(String(20), nullable=False) # stock, future, index
period_type = Column(String(10)) # daily, min1, min5, etc.
start_date = Column(Date, nullable=False)
end_date = Column(Date, nullable=False)
code_list = Column(Text) # 逗号分隔的代码列表
status = Column(String(20), default="pending") # pending, running, completed, failed, cancelled
progress = Column(Numeric(5, 2), default=0)
total_count = Column(Integer, default=0)
success_count = Column(Integer, default=0)
error_count = Column(Integer, default=0)
error_message = Column(Text)
created_by = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
started_at = Column(DateTime(timezone=True))
completed_at = Column(DateTime(timezone=True))
details = relationship("CacheTaskDetail", back_populates="task", cascade="all, delete-orphan")
class CacheTaskDetail(Base):
"""缓存任务详情表"""
__tablename__ = "cache_task_details"
id = Column(BigInteger, primary_key=True, index=True)
task_id = Column(Integer, ForeignKey("cache_tasks.id", ondelete="CASCADE"), nullable=False, index=True)
code = Column(String(20), nullable=False, index=True)
trade_date = Column(Date, nullable=False)
expected_count = Column(Integer, default=0)
actual_count = Column(Integer, default=0)
is_missing = Column(Integer, default=0)
status = Column(String(20), default="pending") # pending, success, failed, skipped
error_message = Column(Text)
processed_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
task = relationship("CacheTask", back_populates="details")

@ -0,0 +1,36 @@
"""
配置模型
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
from app.db.base import Base
class SDKConfig(Base):
"""SDK配置表"""
__tablename__ = "sdk_configs"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
username = Column(String(100), nullable=False)
password = Column(String(255), nullable=False)
host = Column(String(100), nullable=False)
port = Column(Integer, nullable=False, default=8080)
local_path = Column(String(255), default="./amazing_data_cache/")
is_active = Column(Boolean, default=True)
is_default = Column(Boolean, default=False)
description = Column(Text)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
class SystemConfig(Base):
"""系统配置表"""
__tablename__ = "system_configs"
id = Column(Integer, primary_key=True, index=True)
config_key = Column(String(100), unique=True, nullable=False)
config_value = Column(Text, nullable=False)
description = Column(Text)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)

@ -0,0 +1,87 @@
"""
财务数据模型
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, Date, DateTime
from app.db.base import Base
class FinanceBalanceSheet(Base):
"""资产负债表"""
__tablename__ = "finance_balance_sheet"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
report_date = Column(Date, nullable=False, index=True)
report_type = Column(Integer)
statement_type = Column(Integer)
# 资产
total_assets = Column(Numeric(18, 4))
total_cur_assets = Column(Numeric(18, 4))
total_noncur_assets = Column(Numeric(18, 4))
currency_cap = Column(Numeric(18, 4))
notes_receivable = Column(Numeric(18, 4))
acct_receivable = Column(Numeric(18, 4))
inventory = Column(Numeric(18, 4))
fix_assets = Column(Numeric(18, 4))
# 负债
total_liab = Column(Numeric(18, 4))
total_cur_liab = Column(Numeric(18, 4))
total_noncur_liab = Column(Numeric(18, 4))
notes_payable = Column(Numeric(18, 4))
acct_payable = Column(Numeric(18, 4))
st_borrowing = Column(Numeric(18, 4))
lt_loan = Column(Numeric(18, 4))
# 权益
tot_share_equity = Column(Numeric(18, 4))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class FinanceCashFlow(Base):
"""现金流量表"""
__tablename__ = "finance_cash_flow"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
report_date = Column(Date, nullable=False, index=True)
report_type = Column(Integer)
statement_type = Column(Integer)
net_cash_flows_opera_act = Column(Numeric(18, 4))
net_cash_flows_inv_act = Column(Numeric(18, 4))
net_cash_flows_fin_act = Column(Numeric(18, 4))
net_incr_cash_and_cash_equ = Column(Numeric(18, 4))
cash_recp_sg_and_rs = Column(Numeric(18, 4))
cash_pay_goods_services = Column(Numeric(18, 4))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class FinanceIncome(Base):
"""利润表"""
__tablename__ = "finance_income"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
report_date = Column(Date, nullable=False, index=True)
report_type = Column(Integer)
statement_type = Column(Integer)
tot_opera_rev = Column(Numeric(18, 4))
opera_rev = Column(Numeric(18, 4))
tot_opera_cost = Column(Numeric(18, 4))
opera_profit = Column(Numeric(18, 4))
total_profit = Column(Numeric(18, 4))
net_pro_incl_min_int_inc = Column(Numeric(18, 4))
basic_eps = Column(Numeric(12, 6))
diluted_eps = Column(Numeric(12, 6))
rd_exp = Column(Numeric(18, 4))
selling_exp = Column(Numeric(18, 4))
admin_exp = Column(Numeric(18, 4))
fin_exp = Column(Numeric(18, 4))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)

@ -0,0 +1,66 @@
"""
期货数据模型
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, Boolean, Date, DateTime
from app.db.base import Base
class FutureInfo(Base):
"""期货基础信息表"""
__tablename__ = "future_info"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(20), unique=True, nullable=False, index=True)
symbol = Column(String(100), nullable=False)
underlying = Column(String(20))
contract_month = Column(String(10))
pre_close = Column(Numeric(12, 4))
high_limited = Column(Numeric(12, 4))
low_limited = Column(Numeric(12, 4))
price_tick = Column(Numeric(10, 4))
exchange = Column(String(10), default="CFE")
list_date = Column(Date)
expire_date = Column(Date)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
class FutureKlineDaily(Base):
"""期货日线数据表"""
__tablename__ = "future_kline_daily"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
trade_date = Column(Date, nullable=False, index=True)
open = Column(Numeric(12, 4), nullable=False)
high = Column(Numeric(12, 4), nullable=False)
low = Column(Numeric(12, 4), nullable=False)
close = Column(Numeric(12, 4), nullable=False)
volume = Column(BigInteger, nullable=False)
amount = Column(Numeric(18, 4), nullable=False)
settle = Column(Numeric(12, 4))
open_interest = Column(BigInteger)
pre_settle = Column(Numeric(12, 4))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
class FutureKlineMin(Base):
"""期货分钟数据表"""
__tablename__ = "future_kline_min"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
period_type = Column(String(10), nullable=False)
trade_datetime = Column(DateTime, nullable=False, index=True)
open = Column(Numeric(12, 4), nullable=False)
high = Column(Numeric(12, 4), nullable=False)
low = Column(Numeric(12, 4), nullable=False)
close = Column(Numeric(12, 4), nullable=False)
volume = Column(BigInteger, nullable=False)
amount = Column(Numeric(18, 4), nullable=False)
settle = Column(Numeric(12, 4))
open_interest = Column(BigInteger)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)

@ -0,0 +1,60 @@
"""
实时数据模型
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, DateTime
from app.db.base import Base
class RealtimeSnapshot(Base):
"""实时快照数据表"""
__tablename__ = "realtime_snapshot"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
security_type = Column(String(20), nullable=False) # stock, future, index, etf, kzz, option
trade_time = Column(DateTime, nullable=False, index=True)
# 价格数据
pre_close = Column(Numeric(12, 4))
last = Column(Numeric(12, 4))
open = Column(Numeric(12, 4))
high = Column(Numeric(12, 4))
low = Column(Numeric(12, 4))
close = Column(Numeric(12, 4))
volume = Column(BigInteger)
amount = Column(Numeric(18, 4))
# 盘口数据 - 卖盘
ask_price1 = Column(Numeric(12, 4))
ask_price2 = Column(Numeric(12, 4))
ask_price3 = Column(Numeric(12, 4))
ask_price4 = Column(Numeric(12, 4))
ask_price5 = Column(Numeric(12, 4))
ask_volume1 = Column(Integer)
ask_volume2 = Column(Integer)
ask_volume3 = Column(Integer)
ask_volume4 = Column(Integer)
ask_volume5 = Column(Integer)
# 盘口数据 - 买盘
bid_price1 = Column(Numeric(12, 4))
bid_price2 = Column(Numeric(12, 4))
bid_price3 = Column(Numeric(12, 4))
bid_price4 = Column(Numeric(12, 4))
bid_price5 = Column(Numeric(12, 4))
bid_volume1 = Column(Integer)
bid_volume2 = Column(Integer)
bid_volume3 = Column(Integer)
bid_volume4 = Column(Integer)
bid_volume5 = Column(Integer)
# 期货特有字段
settle = Column(Numeric(12, 4))
open_interest = Column(BigInteger)
pre_settle = Column(Numeric(12, 4))
average_price = Column(Numeric(12, 4))
trading_phase_code = Column(String(10))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
expires_at = Column(DateTime(timezone=True), nullable=False, index=True)

@ -0,0 +1,61 @@
"""
股票数据模型
"""
from datetime import datetime, date
from sqlalchemy import Column, Integer, String, BigInteger, Numeric, Boolean, Date, DateTime
from app.db.base import Base
class StockInfo(Base):
"""股票基础信息表"""
__tablename__ = "stock_info"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(20), unique=True, nullable=False, index=True)
symbol = Column(String(100), nullable=False)
security_status = Column(Integer)
pre_close = Column(Numeric(12, 4))
high_limited = Column(Numeric(12, 4))
low_limited = Column(Numeric(12, 4))
price_tick = Column(Numeric(10, 4))
exchange = Column(String(10)) # SH, SZ, BJ
industry = Column(String(50))
list_date = Column(Date)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
class StockKlineDaily(Base):
"""股票日线数据表"""
__tablename__ = "stock_kline_daily"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
trade_date = Column(Date, nullable=False, index=True)
open = Column(Numeric(12, 4), nullable=False)
high = Column(Numeric(12, 4), nullable=False)
low = Column(Numeric(12, 4), nullable=False)
close = Column(Numeric(12, 4), nullable=False)
volume = Column(BigInteger, nullable=False)
amount = Column(Numeric(18, 4), nullable=False)
adj_factor = Column(Numeric(12, 6))
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
class StockKlineMin(Base):
"""股票分钟数据表"""
__tablename__ = "stock_kline_min"
id = Column(BigInteger, primary_key=True, index=True)
code = Column(String(20), nullable=False, index=True)
period_type = Column(String(10), nullable=False) # min1, min5, min15, min30, min60
trade_datetime = Column(DateTime, nullable=False, index=True)
open = Column(Numeric(12, 4), nullable=False)
high = Column(Numeric(12, 4), nullable=False)
low = Column(Numeric(12, 4), nullable=False)
close = Column(Numeric(12, 4), nullable=False)
volume = Column(BigInteger, nullable=False)
amount = Column(Numeric(18, 4), nullable=False)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)

@ -0,0 +1,38 @@
"""
测试日志模型
"""
from datetime import datetime
from sqlalchemy import Column, BigInteger, String, Integer, Boolean, Text, DateTime, JSON, TypeDecorator, String as SQLString
import json
class JSONField(TypeDecorator):
impl = SQLString
def process_bind_param(self, value, dialect):
if value is None:
return None
return json.dumps(value)
def process_result_value(self, value, dialect):
if value is None:
return None
return json.loads(value)
from app.db.base import Base
class APITestLog(Base):
"""API测试日志表"""
__tablename__ = "api_test_logs"
id = Column(BigInteger, primary_key=True, index=True)
test_name = Column(String(200), nullable=False)
api_category = Column(String(50), nullable=False, index=True)
api_endpoint = Column(String(200), nullable=False, index=True)
request_method = Column(String(10), nullable=False)
request_params = Column(JSONField)
response_data = Column(JSONField)
status_code = Column(Integer)
execution_time_ms = Column(Integer)
is_success = Column(Boolean, default=False)
error_message = Column(Text)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow, index=True)

@ -0,0 +1,19 @@
"""
用户模型
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from app.db.base import Base
class User(Base):
"""用户表"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, index=True)
password_hash = Column(String(255), nullable=False)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)

@ -0,0 +1,32 @@
# Schema模块
from app.schemas.base import ResponseModel, PaginatedResponse
from app.schemas.auth import UserLogin, UserInfo, TokenResponse
from app.schemas.config import SDKConfigCreate, SDKConfigUpdate, SDKConfigResponse
from app.schemas.kline import KlineRequest, KlineResponse, KlineChartData
from app.schemas.finance import FinanceRequest, BalanceSheetResponse, CashFlowResponse, IncomeResponse
from app.schemas.cache import CacheTaskCreate, CacheTaskResponse, CacheStatusResponse
from app.schemas.test import TestRequest, TestResponse, TestHistoryResponse
__all__ = [
"ResponseModel",
"PaginatedResponse",
"UserLogin",
"UserInfo",
"TokenResponse",
"SDKConfigCreate",
"SDKConfigUpdate",
"SDKConfigResponse",
"KlineRequest",
"KlineResponse",
"KlineChartData",
"FinanceRequest",
"BalanceSheetResponse",
"CashFlowResponse",
"IncomeResponse",
"CacheTaskCreate",
"CacheTaskResponse",
"CacheStatusResponse",
"TestRequest",
"TestResponse",
"TestHistoryResponse",
]

@ -0,0 +1,30 @@
"""
认证相关Schema
"""
from datetime import datetime
from pydantic import BaseModel
class UserLogin(BaseModel):
"""用户登录请求"""
username: str
password: str
class UserInfo(BaseModel):
"""用户信息"""
id: int
username: str
is_active: bool
is_superuser: bool
created_at: datetime
class Config:
from_attributes = True
class TokenResponse(BaseModel):
"""Token响应"""
access_token: str
token_type: str = "bearer"
expires_in: int

@ -0,0 +1,47 @@
"""
基础Schema
"""
from datetime import datetime
from typing import Optional, TypeVar, Generic, List
from pydantic import BaseModel
T = TypeVar("T")
class ResponseModel(BaseModel, Generic[T]):
"""统一响应模型"""
code: int = 200
message: str = "success"
data: Optional[T] = None
timestamp: datetime = datetime.utcnow()
class Config:
from_attributes = True
class PaginationParams(BaseModel):
"""分页参数"""
page: int = 1
page_size: int = 20
class PaginatedData(BaseModel, Generic[T]):
"""分页数据"""
items: List[T]
total: int
page: int
page_size: int
total_pages: int
class PaginatedResponse(ResponseModel[PaginatedData[T]], Generic[T]):
"""分页响应模型"""
pass
class ErrorResponse(BaseModel):
"""错误响应"""
code: int
message: str
data: Optional[dict] = None
timestamp: datetime = datetime.utcnow()

@ -0,0 +1,108 @@
"""
缓存管理Schema
"""
from datetime import date, datetime
from typing import List, Optional
from pydantic import BaseModel, Field
class CacheTaskCreate(BaseModel):
"""创建缓存任务"""
task_name: str
task_type: str = Field(..., description="detect_missing, cache_data, sync_data")
security_type: str = Field(..., description="stock, future, index")
period_type: Optional[str] = Field(default="daily", description="daily, min1, min5, etc.")
start_date: str
end_date: str
code_list: Optional[List[str]] = None
class MissingDateInfo(BaseModel):
"""缺失日期信息"""
date: str
expected: int
actual: int
missing_ratio: float
class MissingCodeInfo(BaseModel):
"""缺失代码信息"""
code: str
missing_dates: List[MissingDateInfo]
class CacheTaskResponse(BaseModel):
"""缓存任务响应"""
id: int
task_name: str
task_type: str
security_type: str
period_type: Optional[str]
start_date: date
end_date: date
status: str
progress: float
total_count: int
success_count: int
error_count: int
error_message: Optional[str]
created_at: datetime
started_at: Optional[datetime]
completed_at: Optional[datetime]
class Config:
from_attributes = True
class CacheTaskDetailResponse(BaseModel):
"""缓存任务详情响应"""
id: int
code: str
trade_date: date
expected_count: int
actual_count: int
is_missing: bool
status: str
error_message: Optional[str]
processed_at: Optional[datetime]
class CacheTaskWithDetailsResponse(CacheTaskResponse):
"""带详情的缓存任务响应"""
details: List[CacheTaskDetailResponse]
class CacheStatusResponse(BaseModel):
"""代码缓存状态响应"""
code: str
security_type: str
period_type: str
record_count: int
min_date: Optional[str]
max_date: Optional[str]
missing_ratio: float
class DetectMissingRequest(BaseModel):
"""检测缺失数据请求"""
security_type: str
period_type: str = "daily"
start_date: str
end_date: str
code_list: List[str]
class DetectMissingResponse(BaseModel):
"""检测缺失数据响应"""
task_id: int
total_codes: int
missing_codes: List[MissingCodeInfo]
class BatchCacheRequest(BaseModel):
"""批量缓存请求"""
security_type: str
period_type: str = "daily"
start_date: str
end_date: str
code_list: List[str]

@ -0,0 +1,52 @@
"""
配置相关Schema
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
class SDKConfigBase(BaseModel):
"""SDK配置基础"""
name: str
username: str
host: str
port: int = 8080
local_path: str = "./amazing_data_cache/"
is_active: bool = True
is_default: bool = False
description: Optional[str] = None
class SDKConfigCreate(SDKConfigBase):
"""创建SDK配置"""
password: str
class SDKConfigUpdate(BaseModel):
"""更新SDK配置"""
name: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
host: Optional[str] = None
port: Optional[int] = None
local_path: Optional[str] = None
is_active: Optional[bool] = None
is_default: Optional[bool] = None
description: Optional[str] = None
class SDKConfigResponse(SDKConfigBase):
"""SDK配置响应"""
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class SDKConfigTestResponse(BaseModel):
"""SDK配置测试响应"""
success: bool
message: str

@ -0,0 +1,86 @@
"""
财务数据Schema
"""
from datetime import date
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
class FinanceRequest(BaseModel):
"""财务数据请求"""
codes: str = Field(..., description="股票代码,多个用逗号分隔")
start_date: str = Field(..., description="开始报告期(YYYYMMDD)")
end_date: str = Field(..., description="结束报告期(YYYYMMDD)")
class BalanceSheetData(BaseModel):
"""资产负债表数据"""
report_date: str
report_type: Optional[int] = None
statement_type: Optional[int] = None
total_assets: Optional[float] = None
total_cur_assets: Optional[float] = None
total_noncur_assets: Optional[float] = None
currency_cap: Optional[float] = None
notes_receivable: Optional[float] = None
acct_receivable: Optional[float] = None
inventory: Optional[float] = None
fix_assets: Optional[float] = None
total_liab: Optional[float] = None
total_cur_liab: Optional[float] = None
total_noncur_liab: Optional[float] = None
notes_payable: Optional[float] = None
acct_payable: Optional[float] = None
st_borrowing: Optional[float] = None
lt_loan: Optional[float] = None
tot_share_equity: Optional[float] = None
class BalanceSheetResponse(BaseModel):
"""资产负债表响应"""
code: str
data: List[BalanceSheetData]
class CashFlowData(BaseModel):
"""现金流量表数据"""
report_date: str
report_type: Optional[int] = None
statement_type: Optional[int] = None
net_cash_flows_opera_act: Optional[float] = None
net_cash_flows_inv_act: Optional[float] = None
net_cash_flows_fin_act: Optional[float] = None
net_incr_cash_and_cash_equ: Optional[float] = None
cash_recp_sg_and_rs: Optional[float] = None
cash_pay_goods_services: Optional[float] = None
class CashFlowResponse(BaseModel):
"""现金流量表响应"""
code: str
data: List[CashFlowData]
class IncomeData(BaseModel):
"""利润表数据"""
report_date: str
report_type: Optional[int] = None
statement_type: Optional[int] = None
tot_opera_rev: Optional[float] = None
opera_rev: Optional[float] = None
tot_opera_cost: Optional[float] = None
opera_profit: Optional[float] = None
total_profit: Optional[float] = None
net_pro_incl_min_int_inc: Optional[float] = None
basic_eps: Optional[float] = None
diluted_eps: Optional[float] = None
rd_exp: Optional[float] = None
selling_exp: Optional[float] = None
admin_exp: Optional[float] = None
fin_exp: Optional[float] = None
class IncomeResponse(BaseModel):
"""利润表响应"""
code: str
data: List[IncomeData]

@ -0,0 +1,66 @@
"""
K线数据Schema
"""
from datetime import date, datetime
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
class KlineRequest(BaseModel):
"""K线数据请求"""
codes: str = Field(..., description="股票代码,多个用逗号分隔")
start_date: str = Field(..., description="开始日期(YYYYMMDD)")
end_date: str = Field(..., description="结束日期(YYYYMMDD)")
period: str = Field(default="daily", description="周期: daily, min1, min5, min15, min30, min60")
class KlineData(BaseModel):
"""单条K线数据"""
trade_date: Optional[str] = None
trade_datetime: Optional[str] = None
open: float
high: float
low: float
close: float
volume: int
amount: float
settle: Optional[float] = None
open_interest: Optional[int] = None
class KlineResponse(BaseModel):
"""K线数据响应"""
code: str
data: List[KlineData]
class KlineChartData(BaseModel):
"""K线图数据ECharts格式"""
categoryData: List[str]
values: List[List[float]] # [open, close, low, high, volume]
volumes: List[List[float]] # [index, volume, sign]
class BatchKlineRequest(BaseModel):
"""批量K线请求"""
codes: List[str]
start_date: str
end_date: str
period: str = "daily"
class SnapshotData(BaseModel):
"""快照数据"""
trade_time: str
pre_close: Optional[float] = None
last: Optional[float] = None
open: Optional[float] = None
high: Optional[float] = None
low: Optional[float] = None
close: Optional[float] = None
volume: Optional[int] = None
amount: Optional[float] = None
ask_price1: Optional[float] = None
ask_volume1: Optional[int] = None
bid_price1: Optional[float] = None
bid_volume1: Optional[int] = None

@ -0,0 +1,70 @@
"""
测试中心Schema
"""
from datetime import datetime
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
class TestCategory(BaseModel):
"""测试分类"""
key: str
name: str
class TestEndpoint(BaseModel):
"""测试端点"""
category: str
name: str
endpoint: str
method: str
params: Optional[Dict[str, Any]] = None
description: Optional[str] = None
class TestRequest(BaseModel):
"""测试请求"""
endpoint: str
method: str = "GET"
params: Optional[Dict[str, Any]] = None
class TestResponse(BaseModel):
"""测试响应"""
success: bool
endpoint: str
method: str
status_code: Optional[int] = None
execution_time_ms: Optional[int] = None
response_data: Optional[Any] = None
error_message: Optional[str] = None
class TestHistoryResponse(BaseModel):
"""测试历史响应"""
id: int
test_name: str
api_category: str
api_endpoint: str
request_method: str
status_code: Optional[int]
execution_time_ms: Optional[int]
is_success: bool
error_message: Optional[str]
created_at: datetime
class Config:
from_attributes = True
class RunAllTestsRequest(BaseModel):
"""运行全部测试请求"""
categories: List[str] = Field(default=["base_data", "stock", "future", "finance"])
class RunAllTestsResponse(BaseModel):
"""运行全部测试响应"""
total: int
passed: int
failed: int
results: List[TestResponse]

@ -0,0 +1,24 @@
# 服务模块
from app.services.auth_service import AuthService
from app.services.config_service import ConfigService
from app.services.amazing_data_adapter import AmazingDataAdapter
from app.services.cache_service import CacheService
from app.services.stock_service import StockService
from app.services.future_service import FutureService
from app.services.realtime_service import RealtimeService
from app.services.finance_service import FinanceService
from app.services.base_data_service import BaseDataService
from app.services.test_service import TestService
__all__ = [
"AuthService",
"ConfigService",
"AmazingDataAdapter",
"CacheService",
"StockService",
"FutureService",
"RealtimeService",
"FinanceService",
"BaseDataService",
"TestService",
]

@ -0,0 +1,617 @@
"""
AmazingData SDK适配器
封装AmazingData SDK的所有接口
"""
import logging
from typing import List, Dict, Callable, Optional, Tuple
from datetime import date
import pandas as pd
logger = logging.getLogger(__name__)
class AmazingDataAdapter:
"""AmazingData SDK适配器"""
def __init__(self, config: Dict):
"""
初始化适配器
Args:
config: SDK配置字典
- username: 用户名
- password: 密码
- host: 服务器地址
- port: 端口
- local_path: 本地缓存路径
"""
self.config = config
self._ad = None
self._base_data = None
self._market_data = None
self._info_data = None
self._calendar = None
self._is_connected = False
def connect(self) -> bool:
"""
建立与AmazingData SDK的连接
Returns:
是否连接成功
"""
try:
import AmazingData as ad
ad.login(
username=self.config["username"],
password=self.config["password"],
host=self.config["host"],
port=self.config["port"]
)
self._ad = ad
try:
self._base_data = ad.BaseData()
except Exception as e:
logger.error(f"AmazingData SDK登录验证失败: {str(e)}")
return False
self._info_data = ad.InfoData()
self._calendar = self._base_data.get_calendar()
self._market_data = ad.MarketData(self._calendar)
self._is_connected = True
logger.info("AmazingData SDK连接成功")
return True
except ImportError:
logger.error("AmazingData SDK未安装请安装SDK后再试")
return False
except Exception as e:
logger.error(f"AmazingData SDK连接失败: {str(e)}")
return False
def disconnect(self):
"""断开SDK连接"""
if self._is_connected and self._ad:
try:
self._ad.logout(self.config["username"])
except Exception as e:
logger.warning(f"断开SDK连接时出错: {str(e)}")
self._is_connected = False
self._ad = None
self._base_data = None
self._market_data = None
self._info_data = None
self._calendar = None
logger.info("AmazingData SDK已断开")
def is_connected(self) -> bool:
"""检查是否已连接"""
return self._is_connected
# ==================== 基础数据接口 ====================
def get_code_list(self, security_type: str) -> List[str]:
"""
获取代码列表
Args:
security_type: 证券类型
- EXTRA_STOCK_A: 沪深A股
- EXTRA_FUTURE: 期货
- EXTRA_ETF: ETF
- EXTRA_INDEX_A: 指数
Returns:
代码列表
"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
return self._base_data.get_code_list(security_type)
except Exception as e:
logger.error(f"获取代码列表失败: {str(e)}")
return []
def get_code_info(self, security_type: str) -> pd.DataFrame:
"""
获取证券信息
Args:
security_type: 证券类型
Returns:
证券信息DataFrame
"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
return self._base_data.get_code_info(security_type)
except Exception as e:
logger.error(f"获取证券信息失败: {str(e)}")
return pd.DataFrame()
def get_trading_calendar(self, market: str) -> List[int]:
"""
获取交易日历
Args:
market: 市场代码 (SH, SZ, CFE)
Returns:
交易日列表 (YYYYMMDD格式)
"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
if isinstance(self._calendar, list):
return self._calendar
elif hasattr(self._calendar, 'get_trading_calendar'):
return self._calendar.get_trading_calendar(market)
else:
return []
except Exception as e:
logger.error(f"获取交易日历失败: {str(e)}")
return []
def get_adj_factor(self, codes: List[str]) -> pd.DataFrame:
"""获取复权因子"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
return self._base_data.get_adj_factor(codes)
except Exception as e:
logger.error(f"获取复权因子失败: {str(e)}")
return pd.DataFrame()
def get_backward_factor(self, codes: List[str]) -> pd.DataFrame:
"""获取后复权因子"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
return self._base_data.get_backward_factor(codes)
except Exception as e:
logger.error(f"获取后复权因子失败: {str(e)}")
return pd.DataFrame()
# ==================== 历史行情接口 ====================
def get_kline(
self,
codes: List[str],
start_date,
end_date,
period: str = "daily"
) -> Dict[str, pd.DataFrame]:
"""
获取K线数据
Args:
codes: 代码列表
start_date: 开始日期 (YYYYMMDD date对象)
end_date: 结束日期 (YYYYMMDD date对象)
period: 周期 (daily, min1, min5, min15, min30, min60)
Returns:
字典 {code: DataFrame}
"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
# 转换日期格式
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
period_map = {
"daily": 10008,
"min1": 10000,
"min5": 10002,
"min15": 10004,
"min30": 10005,
"min60": 10006,
}
period_value = period_map.get(period, 10008)
try:
return self._market_data.query_kline(codes, start_date, end_date, period_value)
except Exception as e:
logger.error(f"获取K线数据失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
def get_snapshot(
self,
codes: List[str],
start_date,
end_date
) -> Dict[str, pd.DataFrame]:
"""
获取历史快照数据
Args:
codes: 代码列表
start_date: 开始日期
end_date: 结束日期
Returns:
字典 {code: DataFrame}
"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._market_data.query_snapshot(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取快照数据失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
# ==================== 财务数据接口 ====================
def get_balance_sheet(
self,
codes: List[str],
start_date,
end_date
) -> Dict[str, pd.DataFrame]:
"""获取资产负债表"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_balance_sheet(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取资产负债表失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
def get_cash_flow(
self,
codes: List[str],
start_date,
end_date
) -> Dict[str, pd.DataFrame]:
"""获取现金流量表"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_cash_flow(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取现金流量表失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
def get_income_statement(
self,
codes: List[str],
start_date,
end_date
) -> Dict[str, pd.DataFrame]:
"""获取利润表"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_income_statement(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取利润表失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
def get_profit_express(
self,
codes: List[str],
start_date,
end_date
) -> pd.DataFrame:
"""获取业绩快报"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_profit_express(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取业绩快报失败: {str(e)}")
return pd.DataFrame()
def get_profit_notice(
self,
codes: List[str],
start_date,
end_date
) -> pd.DataFrame:
"""获取业绩预告"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_profit_notice(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取业绩预告失败: {str(e)}")
return pd.DataFrame()
# ==================== 股东股本接口 ====================
def get_top10_shareholders(
self,
codes: List[str],
start_date,
end_date
) -> pd.DataFrame:
"""获取十大股东"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_top10_shareholders(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取十大股东失败: {str(e)}")
return pd.DataFrame()
def get_shareholder_count(
self,
codes: List[str],
start_date,
end_date
) -> pd.DataFrame:
"""获取股东户数"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_shareholder_count(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取股东户数失败: {str(e)}")
return pd.DataFrame()
def get_equity_structure(
self,
codes: List[str],
start_date,
end_date
) -> pd.DataFrame:
"""获取股本结构"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_equity_structure(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取股本结构失败: {str(e)}")
return pd.DataFrame()
# ==================== 融资融券接口 ====================
def get_margin_summary(
self,
start_date,
end_date
) -> pd.DataFrame:
"""获取融资融券汇总"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_margin_summary(start_date, end_date)
except Exception as e:
logger.error(f"获取融资融券汇总失败: {str(e)}")
return pd.DataFrame()
def get_margin_detail(
self,
codes: List[str],
start_date,
end_date
) -> Dict[str, pd.DataFrame]:
"""获取融资融券明细"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_margin_detail(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取融资融券明细失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
# ==================== 指数数据接口 ====================
def get_index_constituents(self, codes: List[str]) -> Dict[str, pd.DataFrame]:
"""获取指数成分股"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
return self._info_data.get_index_constituents(codes)
except Exception as e:
logger.error(f"获取指数成分股失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
def get_index_weights(
self,
codes: List[str],
start_date,
end_date
) -> Dict[str, pd.DataFrame]:
"""获取指数权重"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_index_weights(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取指数权重失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
# ==================== ETF数据接口 ====================
def get_etf_pcf(self, codes: List[str]) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
"""获取ETF申赎数据"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
return self._info_data.get_etf_pcf(codes)
except Exception as e:
logger.error(f"获取ETF申赎数据失败: {str(e)}")
return pd.DataFrame(), {}
def get_fund_share(
self,
codes: List[str],
start_date,
end_date
) -> Dict[str, pd.DataFrame]:
"""获取基金份额数据"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
if isinstance(start_date, date):
start_date = int(start_date.strftime("%Y%m%d"))
if isinstance(end_date, date):
end_date = int(end_date.strftime("%Y%m%d"))
try:
return self._info_data.get_fund_share(codes, start_date, end_date)
except Exception as e:
logger.error(f"获取基金份额数据失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
# ==================== 可转债数据接口 ====================
def get_kzz_issuance(self, codes: List[str]) -> Dict[str, pd.DataFrame]:
"""获取可转债发行数据"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
return self._info_data.get_kzz_issuance(codes)
except Exception as e:
logger.error(f"获取可转债发行数据失败: {str(e)}")
return {code: pd.DataFrame() for code in codes}
# ==================== 实时数据订阅 ====================
def subscribe_snapshot(self, codes: List[str], callback: Callable):
"""
订阅实时快照
Args:
codes: 代码列表
callback: 回调函数接收(code, data)参数
"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
# 使用实时数据接口订阅
realtime = self._ad.RealtimeData()
realtime.subscribe_snapshot(codes, callback)
except Exception as e:
logger.error(f"订阅实时快照失败: {str(e)}")
def subscribe_kline(
self,
codes: List[str],
period: str,
callback: Callable
):
"""
订阅实时K线
Args:
codes: 代码列表
period: 周期
callback: 回调函数
"""
if not self._is_connected:
raise RuntimeError("SDK未连接")
try:
realtime = self._ad.RealtimeData()
realtime.subscribe_kline(codes, period, callback)
except Exception as e:
logger.error(f"订阅实时K线失败: {str(e)}")
def unsubscribe(self, codes: List[str] = None):
"""
取消订阅
Args:
codes: 代码列表None表示取消所有
"""
if not self._is_connected:
return
try:
realtime = self._ad.RealtimeData()
realtime.unsubscribe(codes)
except Exception as e:
logger.error(f"取消订阅失败: {str(e)}")

@ -0,0 +1,135 @@
"""
认证服务
"""
from datetime import timedelta
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from app.models.user import User
from app.core.security import verify_password, get_password_hash, create_access_token
from app.config import settings
class AuthService:
"""认证服务"""
@staticmethod
def authenticate_user(db: Session, username: str, password: str) -> User:
"""
验证用户凭据
Args:
db: 数据库会话
username: 用户名
password: 密码
Returns:
用户对象
Raises:
HTTPException: 认证失败
"""
user = db.query(User).filter(User.username == username).first()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误"
)
if not verify_password(password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="用户已被禁用"
)
return user
@staticmethod
def create_user_token(user: User) -> dict:
"""
创建用户访问令牌
Args:
user: 用户对象
Returns:
Token信息
"""
access_token_expires = timedelta(hours=settings.ACCESS_TOKEN_EXPIRE_HOURS)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"expires_in": settings.ACCESS_TOKEN_EXPIRE_HOURS * 3600
}
@staticmethod
def get_user_by_username(db: Session, username: str) -> User:
"""通过用户名获取用户"""
return db.query(User).filter(User.username == username).first()
@staticmethod
def create_user(db: Session, username: str, password: str, is_superuser: bool = False) -> User:
"""
创建新用户
Args:
db: 数据库会话
username: 用户名
password: 密码
is_superuser: 是否超级用户
Returns:
新用户对象
"""
# 检查用户名是否已存在
existing_user = db.query(User).filter(User.username == username).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户名已存在"
)
# 创建新用户
user = User(
username=username,
password_hash=get_password_hash(password),
is_superuser=is_superuser
)
db.add(user)
db.commit()
db.refresh(user)
return user
@staticmethod
def change_password(db: Session, user: User, old_password: str, new_password: str):
"""
修改密码
Args:
db: 数据库会话
user: 用户对象
old_password: 旧密码
new_password: 新密码
"""
if not verify_password(old_password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="旧密码错误"
)
user.password_hash = get_password_hash(new_password)
db.commit()

@ -0,0 +1,120 @@
"""
基础数据服务
"""
from typing import List, Optional, Dict
from datetime import date
from sqlalchemy.orm import Session
import pandas as pd
from app.services.sdk_manager import sdk_manager
from app.models.config import SDKConfig
class BaseDataService:
"""基础数据服务"""
def __init__(self, db: Session):
self.db = db
def _get_adapter(self):
"""获取SDK适配器使用连接管理器"""
return sdk_manager.get_default_connection()
def get_code_list(self, security_type: str) -> List[str]:
"""
获取代码列表
Args:
security_type: 证券类型
- EXTRA_STOCK_A: 沪深A股
- EXTRA_FUTURE: 期货
- EXTRA_ETF: ETF
- EXTRA_INDEX_A: 指数
Returns:
代码列表
"""
adapter = self._get_adapter()
if not adapter:
raise RuntimeError("SDK连接失败请先测试连接")
return adapter.get_code_list(security_type)
def get_code_info(self, security_type: str) -> pd.DataFrame:
"""获取证券信息"""
adapter = self._get_adapter()
if not adapter:
raise RuntimeError("SDK连接失败请先测试连接")
return adapter.get_code_info(security_type)
def get_trading_calendar(
self,
market: str,
start_date: date = None,
end_date: date = None
) -> List[date]:
"""
获取交易日历
Args:
market: 市场代码 (SH, SZ, CFE)
start_date: 开始日期
end_date: 结束日期
Returns:
交易日列表
"""
adapter = self._get_adapter()
if not adapter:
raise RuntimeError("SDK连接失败请先测试连接")
calendar_ints = adapter.get_trading_calendar(market)
# 转换为date对象
from app.utils.date_utils import int_to_date
dates = [int_to_date(d) for d in calendar_ints]
# 过滤日期范围
if start_date:
dates = [d for d in dates if d >= start_date]
if end_date:
dates = [d for d in dates if d <= end_date]
return dates
def get_adj_factor(self, codes: List[str]) -> pd.DataFrame:
"""获取复权因子"""
adapter = self._get_adapter()
if not adapter:
raise RuntimeError("SDK连接失败请先测试连接")
return adapter.get_adj_factor(codes)
def get_backward_factor(self, codes: List[str]) -> pd.DataFrame:
"""获取后复权因子"""
adapter = self._get_adapter()
if not adapter:
raise RuntimeError("SDK连接失败请先测试连接")
return adapter.get_backward_factor(codes)
def get_security_type(self, code: str) -> str:
"""
根据代码判断证券类型
Args:
code: 证券代码
Returns:
证券类型 (stock, future, index, etf, unknown)
"""
if code.endswith(".CFE"):
return "future"
elif code.endswith((".SH", ".SZ", ".BJ")):
# 根据代码前缀判断
prefix = code[:3]
if prefix in ["000", "001", "002", "003", "300", "301", "600", "601", "603", "605", "688"]:
return "stock"
elif prefix in ["510", "511", "512", "513", "515", "516", "518", "560", "563", "588"]:
return "etf"
elif prefix in ["000", "880"]:
return "index"
return "unknown"

@ -0,0 +1,303 @@
"""
缓存管理服务
"""
import logging
from typing import List, Dict, Optional
from datetime import date, datetime
from sqlalchemy.orm import Session
from sqlalchemy import and_, func
from app.models.cache import CacheTask, CacheTaskDetail
from app.models.stock import StockKlineDaily
from app.models.future import FutureKlineDaily
from app.services.base_data_service import BaseDataService
from app.services.stock_service import StockService
from app.services.future_service import FutureService
from app.utils.date_utils import parse_date, format_date, get_market_from_code
from app.config import settings
logger = logging.getLogger(__name__)
class CacheService:
"""缓存服务"""
def __init__(self, db: Session):
self.db = db
self.base_service = BaseDataService(db)
self.stock_service = StockService(db)
self.future_service = FutureService(db)
def detect_missing_data(
self,
security_type: str,
period_type: str,
start_date: date,
end_date: date,
code_list: List[str]
) -> CacheTask:
"""
检测缺失数据
Args:
security_type: 证券类型 (stock, future)
period_type: 周期类型 (daily, min1, etc.)
start_date: 开始日期
end_date: 结束日期
code_list: 代码列表
Returns:
缓存任务对象
"""
# 创建检测任务
task = CacheTask(
task_name=f"检测缺失数据 - {security_type} - {len(code_list)}个代码",
task_type="detect_missing",
security_type=security_type,
period_type=period_type,
start_date=start_date,
end_date=end_date,
code_list=",".join(code_list),
status="running",
total_count=len(code_list),
started_at=datetime.utcnow()
)
self.db.add(task)
self.db.commit()
self.db.refresh(task)
try:
# 获取交易日历
market = "CFE" if security_type == "future" else "SH"
trading_days = self.base_service.get_trading_calendar(market, start_date, end_date)
expected_count = len(trading_days)
success_count = 0
error_count = 0
for code in code_list:
try:
# 查询实际数据量
if security_type == "stock" and period_type == "daily":
actual_count = self.db.query(StockKlineDaily).filter(
and_(
StockKlineDaily.code == code,
StockKlineDaily.trade_date >= start_date,
StockKlineDaily.trade_date <= end_date
)
).count()
elif security_type == "future" and period_type == "daily":
actual_count = self.db.query(FutureKlineDaily).filter(
and_(
FutureKlineDaily.code == code,
FutureKlineDaily.trade_date >= start_date,
FutureKlineDaily.trade_date <= end_date
)
).count()
else:
actual_count = 0
# 计算缺失率
missing_ratio = 0
if expected_count > 0:
missing_ratio = (expected_count - actual_count) / expected_count
is_missing = missing_ratio > settings.CACHE_MISSING_THRESHOLD
# 创建任务详情
detail = CacheTaskDetail(
task_id=task.id,
code=code,
trade_date=start_date,
expected_count=expected_count,
actual_count=actual_count,
is_missing=1 if is_missing else 0,
status="pending" if is_missing else "skipped"
)
self.db.add(detail)
if is_missing:
success_count += 1
except Exception as e:
logger.error(f"检测{code}缺失数据失败: {str(e)}")
error_count += 1
detail = CacheTaskDetail(
task_id=task.id,
code=code,
trade_date=start_date,
status="failed",
error_message=str(e)
)
self.db.add(detail)
# 更新进度
task.success_count = success_count
task.error_count = error_count
task.progress = min(100, int((success_count + error_count) / len(code_list) * 100))
self.db.commit()
task.status = "completed"
task.completed_at = datetime.utcnow()
self.db.commit()
except Exception as e:
task.status = "failed"
task.error_message = str(e)
task.completed_at = datetime.utcnow()
self.db.commit()
logger.error(f"检测缺失数据任务失败: {str(e)}")
return task
def batch_cache_data(
self,
security_type: str,
period_type: str,
start_date: date,
end_date: date,
code_list: List[str]
) -> CacheTask:
"""
批量缓存数据
Args:
security_type: 证券类型
period_type: 周期类型
start_date: 开始日期
end_date: 结束日期
code_list: 代码列表
Returns:
缓存任务对象
"""
# 创建缓存任务
task = CacheTask(
task_name=f"批量缓存数据 - {security_type} - {len(code_list)}个代码",
task_type="cache_data",
security_type=security_type,
period_type=period_type,
start_date=start_date,
end_date=end_date,
code_list=",".join(code_list),
status="running",
total_count=len(code_list),
started_at=datetime.utcnow()
)
self.db.add(task)
self.db.commit()
self.db.refresh(task)
try:
success_count = 0
error_count = 0
for code in code_list:
try:
# 获取数据(会自动缓存)
if security_type == "stock":
self.stock_service.get_kline([code], start_date, end_date, period_type)
elif security_type == "future":
self.future_service.get_kline([code], start_date, end_date, period_type)
success_count += 1
# 创建任务详情
detail = CacheTaskDetail(
task_id=task.id,
code=code,
trade_date=start_date,
status="success",
processed_at=datetime.utcnow()
)
self.db.add(detail)
except Exception as e:
logger.error(f"缓存{code}数据失败: {str(e)}")
error_count += 1
detail = CacheTaskDetail(
task_id=task.id,
code=code,
trade_date=start_date,
status="failed",
error_message=str(e)
)
self.db.add(detail)
# 更新进度
task.success_count = success_count
task.error_count = error_count
task.progress = min(100, int((success_count + error_count) / len(code_list) * 100))
self.db.commit()
task.status = "completed"
task.completed_at = datetime.utcnow()
self.db.commit()
except Exception as e:
task.status = "failed"
task.error_message = str(e)
task.completed_at = datetime.utcnow()
self.db.commit()
logger.error(f"批量缓存数据任务失败: {str(e)}")
return task
def get_tasks(
self,
page: int = 1,
page_size: int = 20
) -> Dict:
"""获取缓存任务列表"""
query = self.db.query(CacheTask).order_by(CacheTask.created_at.desc())
total = query.count()
tasks = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"items": tasks,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size
}
def get_task(self, task_id: int) -> Optional[CacheTask]:
"""获取任务详情"""
return self.db.query(CacheTask).filter(CacheTask.id == task_id).first()
def get_task_details(self, task_id: int) -> List[CacheTaskDetail]:
"""获取任务详情列表"""
return self.db.query(CacheTaskDetail).filter(
CacheTaskDetail.task_id == task_id
).all()
def cancel_task(self, task_id: int) -> bool:
"""取消任务"""
task = self.db.query(CacheTask).filter(CacheTask.id == task_id).first()
if task and task.status == "running":
task.status = "cancelled"
task.completed_at = datetime.utcnow()
self.db.commit()
return True
return False
def get_cache_status(self, code: str, security_type: str, period_type: str) -> Dict:
"""获取代码缓存状态"""
if security_type == "stock":
return self.stock_service.get_cache_status(code, period_type)
elif security_type == "future":
return self.future_service.get_cache_status(code, period_type)
else:
return {
"code": code,
"security_type": security_type,
"period_type": period_type,
"record_count": 0,
"min_date": None,
"max_date": None
}

@ -0,0 +1,149 @@
"""
配置服务
"""
from typing import List, Optional
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from app.models.config import SDKConfig, SystemConfig
class ConfigService:
"""配置服务"""
@staticmethod
def get_sdk_configs(db: Session) -> List[SDKConfig]:
"""获取所有SDK配置"""
return db.query(SDKConfig).order_by(SDKConfig.created_at.desc()).all()
@staticmethod
def get_sdk_config(db: Session, config_id: int) -> Optional[SDKConfig]:
"""获取指定SDK配置"""
return db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
@staticmethod
def get_default_sdk_config(db: Session) -> Optional[SDKConfig]:
"""获取默认SDK配置"""
return db.query(SDKConfig).filter(SDKConfig.is_default == True).first()
@staticmethod
def create_sdk_config(db: Session, config_data: dict) -> SDKConfig:
"""
创建SDK配置
Args:
db: 数据库会话
config_data: 配置数据
Returns:
新配置对象
"""
config = SDKConfig(**config_data)
db.add(config)
db.commit()
db.refresh(config)
return config
@staticmethod
def update_sdk_config(
db: Session,
config_id: int,
config_data: dict
) -> SDKConfig:
"""
更新SDK配置
Args:
db: 数据库会话
config_id: 配置ID
config_data: 更新数据
Returns:
更新后的配置对象
"""
config = db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="配置不存在"
)
# 更新字段
for key, value in config_data.items():
if value is not None and hasattr(config, key):
setattr(config, key, value)
db.commit()
db.refresh(config)
return config
@staticmethod
def delete_sdk_config(db: Session, config_id: int):
"""
删除SDK配置
Args:
db: 数据库会话
config_id: 配置ID
"""
config = db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="配置不存在"
)
db.delete(config)
db.commit()
@staticmethod
def set_default_config(db: Session, config_id: int):
"""
设置默认配置
Args:
db: 数据库会话
config_id: 配置ID
"""
config = db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="配置不存在"
)
# 取消其他配置的默认状态
db.query(SDKConfig).update({SDKConfig.is_default: False})
# 设置当前配置为默认
config.is_default = True
db.commit()
db.refresh(config)
@staticmethod
def get_system_config(db: Session, key: str) -> Optional[str]:
"""获取系统配置值"""
config = db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
return config.config_value if config else None
@staticmethod
def set_system_config(db: Session, key: str, value: str, description: str = None):
"""设置系统配置"""
config = db.query(SystemConfig).filter(SystemConfig.config_key == key).first()
if config:
config.config_value = value
if description:
config.description = description
else:
config = SystemConfig(
config_key=key,
config_value=value,
description=description
)
db.add(config)
db.commit()

@ -0,0 +1,322 @@
"""
财务数据服务
"""
from typing import List, Dict
from datetime import date
from sqlalchemy.orm import Session
from sqlalchemy import and_
import pandas as pd
import logging
from app.models.finance import FinanceBalanceSheet, FinanceCashFlow, FinanceIncome
from app.services.sdk_manager import sdk_manager
from app.services.base_data_service import BaseDataService
from app.utils.date_utils import parse_date, format_date
logger = logging.getLogger(__name__)
class FinanceService:
"""财务数据服务"""
def __init__(self, db: Session):
self.db = db
self.base_service = BaseDataService(db)
def _get_adapter(self):
"""获取SDK适配器使用连接管理器"""
return sdk_manager.get_default_connection()
def get_balance_sheet(
self,
codes: List[str],
start_date: date,
end_date: date
) -> Dict[str, List[dict]]:
"""获取资产负债表"""
result = {}
for code in codes:
try:
# 查询本地缓存
records = self.db.query(FinanceBalanceSheet).filter(
and_(
FinanceBalanceSheet.code == code,
FinanceBalanceSheet.report_date >= start_date,
FinanceBalanceSheet.report_date <= end_date
)
).order_by(FinanceBalanceSheet.report_date.desc()).all()
# 如果本地没有数据从SDK获取
if not records:
adapter = self._get_adapter()
if adapter:
sdk_data = adapter.get_balance_sheet([code], start_date, end_date)
if code in sdk_data and not sdk_data[code].empty:
self._save_balance_sheet(code, sdk_data[code])
records = self.db.query(FinanceBalanceSheet).filter(
and_(
FinanceBalanceSheet.code == code,
FinanceBalanceSheet.report_date >= start_date,
FinanceBalanceSheet.report_date <= end_date
)
).order_by(FinanceBalanceSheet.report_date.desc()).all()
result[code] = [
{
"report_date": format_date(r.report_date),
"report_type": r.report_type,
"statement_type": r.statement_type,
"total_assets": float(r.total_assets) if r.total_assets else None,
"total_cur_assets": float(r.total_cur_assets) if r.total_cur_assets else None,
"total_noncur_assets": float(r.total_noncur_assets) if r.total_noncur_assets else None,
"currency_cap": float(r.currency_cap) if r.currency_cap else None,
"notes_receivable": float(r.notes_receivable) if r.notes_receivable else None,
"acct_receivable": float(r.acct_receivable) if r.acct_receivable else None,
"inventory": float(r.inventory) if r.inventory else None,
"fix_assets": float(r.fix_assets) if r.fix_assets else None,
"total_liab": float(r.total_liab) if r.total_liab else None,
"total_cur_liab": float(r.total_cur_liab) if r.total_cur_liab else None,
"total_noncur_liab": float(r.total_noncur_liab) if r.total_noncur_liab else None,
"notes_payable": float(r.notes_payable) if r.notes_payable else None,
"acct_payable": float(r.acct_payable) if r.acct_payable else None,
"st_borrowing": float(r.st_borrowing) if r.st_borrowing else None,
"lt_loan": float(r.lt_loan) if r.lt_loan else None,
"tot_share_equity": float(r.tot_share_equity) if r.tot_share_equity else None
}
for r in records
]
except Exception as e:
logger.error(f"获取{code}资产负债表失败: {str(e)}")
result[code] = []
return result
def get_cash_flow(
self,
codes: List[str],
start_date: date,
end_date: date
) -> Dict[str, List[dict]]:
"""获取现金流量表"""
result = {}
for code in codes:
try:
records = self.db.query(FinanceCashFlow).filter(
and_(
FinanceCashFlow.code == code,
FinanceCashFlow.report_date >= start_date,
FinanceCashFlow.report_date <= end_date
)
).order_by(FinanceCashFlow.report_date.desc()).all()
if not records:
adapter = self._get_adapter()
if adapter:
sdk_data = adapter.get_cash_flow([code], start_date, end_date)
if code in sdk_data and not sdk_data[code].empty:
self._save_cash_flow(code, sdk_data[code])
records = self.db.query(FinanceCashFlow).filter(
and_(
FinanceCashFlow.code == code,
FinanceCashFlow.report_date >= start_date,
FinanceCashFlow.report_date <= end_date
)
).order_by(FinanceCashFlow.report_date.desc()).all()
result[code] = [
{
"report_date": format_date(r.report_date),
"report_type": r.report_type,
"statement_type": r.statement_type,
"net_cash_flows_opera_act": float(r.net_cash_flows_opera_act) if r.net_cash_flows_opera_act else None,
"net_cash_flows_inv_act": float(r.net_cash_flows_inv_act) if r.net_cash_flows_inv_act else None,
"net_cash_flows_fin_act": float(r.net_cash_flows_fin_act) if r.net_cash_flows_fin_act else None,
"net_incr_cash_and_cash_equ": float(r.net_incr_cash_and_cash_equ) if r.net_incr_cash_and_cash_equ else None,
"cash_recp_sg_and_rs": float(r.cash_recp_sg_and_rs) if r.cash_recp_sg_and_rs else None,
"cash_pay_goods_services": float(r.cash_pay_goods_services) if r.cash_pay_goods_services else None
}
for r in records
]
except Exception as e:
logger.error(f"获取{code}现金流量表失败: {str(e)}")
result[code] = []
return result
def get_income_statement(
self,
codes: List[str],
start_date: date,
end_date: date
) -> Dict[str, List[dict]]:
"""获取利润表"""
result = {}
for code in codes:
try:
records = self.db.query(FinanceIncome).filter(
and_(
FinanceIncome.code == code,
FinanceIncome.report_date >= start_date,
FinanceIncome.report_date <= end_date
)
).order_by(FinanceIncome.report_date.desc()).all()
if not records:
adapter = self._get_adapter()
if adapter:
sdk_data = adapter.get_income_statement([code], start_date, end_date)
if code in sdk_data and not sdk_data[code].empty:
self._save_income(code, sdk_data[code])
records = self.db.query(FinanceIncome).filter(
and_(
FinanceIncome.code == code,
FinanceIncome.report_date >= start_date,
FinanceIncome.report_date <= end_date
)
).order_by(FinanceIncome.report_date.desc()).all()
result[code] = [
{
"report_date": format_date(r.report_date),
"report_type": r.report_type,
"statement_type": r.statement_type,
"tot_opera_rev": float(r.tot_opera_rev) if r.tot_opera_rev else None,
"opera_rev": float(r.opera_rev) if r.opera_rev else None,
"tot_opera_cost": float(r.tot_opera_cost) if r.tot_opera_cost else None,
"opera_profit": float(r.opera_profit) if r.opera_profit else None,
"total_profit": float(r.total_profit) if r.total_profit else None,
"net_pro_incl_min_int_inc": float(r.net_pro_incl_min_int_inc) if r.net_pro_incl_min_int_inc else None,
"basic_eps": float(r.basic_eps) if r.basic_eps else None,
"diluted_eps": float(r.diluted_eps) if r.diluted_eps else None,
"rd_exp": float(r.rd_exp) if r.rd_exp else None,
"selling_exp": float(r.selling_exp) if r.selling_exp else None,
"admin_exp": float(r.admin_exp) if r.admin_exp else None,
"fin_exp": float(r.fin_exp) if r.fin_exp else None
}
for r in records
]
except Exception as e:
logger.error(f"获取{code}利润表失败: {str(e)}")
result[code] = []
return result
def _save_balance_sheet(self, code: str, df: pd.DataFrame):
"""保存资产负债表"""
if df.empty:
return
for idx, row in df.iterrows():
report_date = idx if isinstance(idx, date) else parse_date(str(idx))
statement_type = int(row.get("statement_type", 0))
existing = self.db.query(FinanceBalanceSheet).filter(
and_(
FinanceBalanceSheet.code == code,
FinanceBalanceSheet.report_date == report_date,
FinanceBalanceSheet.statement_type == statement_type
)
).first()
def get_float(val):
if pd.isna(val):
return None
return float(val)
if existing:
existing.total_assets = get_float(row.get("total_assets"))
existing.total_cur_assets = get_float(row.get("total_cur_assets"))
existing.total_noncur_assets = get_float(row.get("total_noncur_assets"))
existing.tot_share_equity = get_float(row.get("tot_share_equity"))
else:
record = FinanceBalanceSheet(
code=code,
report_date=report_date,
report_type=get_float(row.get("report_type")),
statement_type=statement_type,
total_assets=get_float(row.get("total_assets")),
total_cur_assets=get_float(row.get("total_cur_assets")),
tot_share_equity=get_float(row.get("tot_share_equity"))
)
self.db.add(record)
self.db.commit()
def _save_cash_flow(self, code: str, df: pd.DataFrame):
"""保存现金流量表"""
if df.empty:
return
for idx, row in df.iterrows():
report_date = idx if isinstance(idx, date) else parse_date(str(idx))
statement_type = int(row.get("statement_type", 0))
existing = self.db.query(FinanceCashFlow).filter(
and_(
FinanceCashFlow.code == code,
FinanceCashFlow.report_date == report_date,
FinanceCashFlow.statement_type == statement_type
)
).first()
if not existing:
def get_float(val):
if pd.isna(val):
return None
return float(val)
record = FinanceCashFlow(
code=code,
report_date=report_date,
report_type=get_float(row.get("report_type")),
statement_type=statement_type,
net_cash_flows_opera_act=get_float(row.get("net_cash_flows_opera_act"))
)
self.db.add(record)
self.db.commit()
def _save_income(self, code: str, df: pd.DataFrame):
"""保存利润表"""
if df.empty:
return
for idx, row in df.iterrows():
report_date = idx if isinstance(idx, date) else parse_date(str(idx))
statement_type = int(row.get("statement_type", 0))
existing = self.db.query(FinanceIncome).filter(
and_(
FinanceIncome.code == code,
FinanceIncome.report_date == report_date,
FinanceIncome.statement_type == statement_type
)
).first()
if not existing:
def get_float(val):
if pd.isna(val):
return None
return float(val)
record = FinanceIncome(
code=code,
report_date=report_date,
report_type=get_float(row.get("report_type")),
statement_type=statement_type,
tot_opera_rev=get_float(row.get("tot_opera_rev")),
net_pro_incl_min_int_inc=get_float(row.get("net_pro_incl_min_int_inc")),
basic_eps=get_float(row.get("basic_eps"))
)
self.db.add(record)
self.db.commit()

@ -0,0 +1,327 @@
"""
期货数据服务
"""
from typing import List, Dict
from datetime import date
from sqlalchemy.orm import Session
from sqlalchemy import and_
import pandas as pd
import logging
from app.models.future import FutureKlineDaily, FutureKlineMin
from app.services.sdk_manager import sdk_manager
from app.services.base_data_service import BaseDataService
from app.utils.date_utils import parse_date, format_date
logger = logging.getLogger(__name__)
class FutureService:
"""期货数据服务"""
def __init__(self, db: Session):
self.db = db
self.base_service = BaseDataService(db)
def _get_adapter(self):
"""获取SDK适配器使用连接管理器"""
return sdk_manager.get_default_connection()
def get_kline(
self,
codes: List[str],
start_date: date,
end_date: date,
period: str = "daily"
) -> Dict[str, List[dict]]:
"""
获取期货K线数据带缓存
Args:
codes: 代码列表
start_date: 开始日期
end_date: 结束日期
period: 周期 (daily, min1, min5, min15, min30, min60)
Returns:
字典 {code: [kline_data]}
"""
result = {}
for code in codes:
try:
if period == "daily":
data = self._get_daily_kline_with_cache(code, start_date, end_date)
else:
data = self._get_min_kline_with_cache(code, start_date, end_date, period)
result[code] = data
except Exception as e:
logger.error(f"获取{code}的K线数据失败: {str(e)}")
result[code] = []
return result
def _get_daily_kline_with_cache(
self,
code: str,
start_date: date,
end_date: date
) -> List[dict]:
"""获取期货日线数据(带缓存)"""
# 1. 查询本地缓存
cached_records = self.db.query(FutureKlineDaily).filter(
and_(
FutureKlineDaily.code == code,
FutureKlineDaily.trade_date >= start_date,
FutureKlineDaily.trade_date <= end_date
)
).order_by(FutureKlineDaily.trade_date).all()
# 2. 检查数据完整性
cached_dates = {r.trade_date for r in cached_records}
expected_dates = set(self.base_service.get_trading_calendar("CFE", start_date, end_date))
missing_dates = expected_dates - cached_dates
# 3. 如果有缺失从SDK获取
if missing_dates:
try:
adapter = self._get_adapter()
if adapter:
sdk_data = adapter.get_kline([code], start_date, end_date, "daily")
if code in sdk_data and not sdk_data[code].empty:
self._save_daily_kline(code, sdk_data[code])
cached_records = self.db.query(FutureKlineDaily).filter(
and_(
FutureKlineDaily.code == code,
FutureKlineDaily.trade_date >= start_date,
FutureKlineDaily.trade_date <= end_date
)
).order_by(FutureKlineDaily.trade_date).all()
except Exception as e:
logger.error(f"从SDK获取{code}数据失败: {str(e)}")
return [
{
"trade_date": format_date(r.trade_date),
"open": float(r.open),
"high": float(r.high),
"low": float(r.low),
"close": float(r.close),
"volume": int(r.volume),
"amount": float(r.amount),
"settle": float(r.settle) if r.settle else None,
"open_interest": int(r.open_interest) if r.open_interest else None
}
for r in cached_records
]
def _get_min_kline_with_cache(
self,
code: str,
start_date: date,
end_date: date,
period: str
) -> List[dict]:
"""获取期货分钟线数据(带缓存)"""
from datetime import datetime
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
cached_records = self.db.query(FutureKlineMin).filter(
and_(
FutureKlineMin.code == code,
FutureKlineMin.period_type == period,
FutureKlineMin.trade_datetime >= start_datetime,
FutureKlineMin.trade_datetime <= end_datetime
)
).order_by(FutureKlineMin.trade_datetime).all()
if len(cached_records) < 10:
try:
adapter = self._get_adapter()
if adapter:
sdk_data = adapter.get_kline([code], start_date, end_date, period)
if code in sdk_data and not sdk_data[code].empty:
self._save_min_kline(code, sdk_data[code], period)
cached_records = self.db.query(FutureKlineMin).filter(
and_(
FutureKlineMin.code == code,
FutureKlineMin.period_type == period,
FutureKlineMin.trade_datetime >= start_datetime,
FutureKlineMin.trade_datetime <= end_datetime
)
).order_by(FutureKlineMin.trade_datetime).all()
except Exception as e:
logger.error(f"从SDK获取{code}分钟数据失败: {str(e)}")
return [
{
"trade_datetime": r.trade_datetime.isoformat(),
"open": float(r.open),
"high": float(r.high),
"low": float(r.low),
"close": float(r.close),
"volume": int(r.volume),
"amount": float(r.amount),
"settle": float(r.settle) if r.settle else None,
"open_interest": int(r.open_interest) if r.open_interest else None
}
for r in cached_records
]
def _save_daily_kline(self, code: str, df: pd.DataFrame):
"""保存期货日线数据到数据库"""
if df.empty:
return
for idx, row in df.iterrows():
trade_date = idx if isinstance(idx, date) else parse_date(str(idx))
existing = self.db.query(FutureKlineDaily).filter(
and_(
FutureKlineDaily.code == code,
FutureKlineDaily.trade_date == trade_date
)
).first()
if existing:
existing.open = float(row.get("open", 0))
existing.high = float(row.get("high", 0))
existing.low = float(row.get("low", 0))
existing.close = float(row.get("close", 0))
existing.volume = int(row.get("volume", 0))
existing.amount = float(row.get("amount", 0))
existing.settle = float(row.get("settle")) if pd.notna(row.get("settle")) else None
existing.open_interest = int(row.get("open_interest")) if pd.notna(row.get("open_interest")) else None
else:
record = FutureKlineDaily(
code=code,
trade_date=trade_date,
open=float(row.get("open", 0)),
high=float(row.get("high", 0)),
low=float(row.get("low", 0)),
close=float(row.get("close", 0)),
volume=int(row.get("volume", 0)),
amount=float(row.get("amount", 0)),
settle=float(row.get("settle")) if pd.notna(row.get("settle")) else None,
open_interest=int(row.get("open_interest")) if pd.notna(row.get("open_interest")) else None
)
self.db.add(record)
self.db.commit()
def _save_min_kline(self, code: str, df: pd.DataFrame, period: str):
"""保存期货分钟线数据到数据库"""
if df.empty:
return
from datetime import datetime
for idx, row in df.iterrows():
trade_datetime = idx if isinstance(idx, datetime) else datetime.fromisoformat(str(idx))
existing = self.db.query(FutureKlineMin).filter(
and_(
FutureKlineMin.code == code,
FutureKlineMin.period_type == period,
FutureKlineMin.trade_datetime == trade_datetime
)
).first()
if not existing:
record = FutureKlineMin(
code=code,
period_type=period,
trade_datetime=trade_datetime,
open=float(row.get("open", 0)),
high=float(row.get("high", 0)),
low=float(row.get("low", 0)),
close=float(row.get("close", 0)),
volume=int(row.get("volume", 0)),
amount=float(row.get("amount", 0)),
settle=float(row.get("settle")) if pd.notna(row.get("settle")) else None,
open_interest=int(row.get("open_interest")) if pd.notna(row.get("open_interest")) else None
)
self.db.add(record)
self.db.commit()
def get_kline_chart_data(
self,
code: str,
start_date: date,
end_date: date,
period: str = "daily"
) -> dict:
"""获取K线图数据ECharts格式"""
kline_data = self.get_kline([code], start_date, end_date, period)
data = kline_data.get(code, [])
if not data:
return {
"categoryData": [],
"values": [],
"volumes": []
}
category_data = []
values = []
volumes = []
for i, item in enumerate(data):
date_key = item.get("trade_date") or item.get("trade_datetime", "")[:10]
category_data.append(date_key)
values.append([
item["open"],
item["close"],
item["low"],
item["high"],
item["volume"]
])
sign = 1 if item["close"] >= item["open"] else -1
volumes.append([i, item["volume"], sign])
return {
"categoryData": category_data,
"values": values,
"volumes": volumes
}
def get_cache_status(self, code: str, period: str = "daily") -> dict:
"""获取代码缓存状态"""
if period == "daily":
query = self.db.query(FutureKlineDaily).filter(FutureKlineDaily.code == code)
count = query.count()
min_date = query.order_by(FutureKlineDaily.trade_date).first()
max_date = query.order_by(FutureKlineDaily.trade_date.desc()).first()
return {
"code": code,
"security_type": "future",
"period_type": period,
"record_count": count,
"min_date": format_date(min_date.trade_date) if min_date else None,
"max_date": format_date(max_date.trade_date) if max_date else None
}
else:
query = self.db.query(FutureKlineMin).filter(
FutureKlineMin.code == code,
FutureKlineMin.period_type == period
)
count = query.count()
return {
"code": code,
"security_type": "future",
"period_type": period,
"record_count": count,
"min_date": None,
"max_date": None
}

@ -0,0 +1,204 @@
"""
实时数据服务
"""
import asyncio
import logging
from typing import Dict, Set, List, Callable, Optional
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from fastapi import WebSocket
from app.models.realtime import RealtimeSnapshot
from app.services.base_data_service import BaseDataService
from app.config import settings
logger = logging.getLogger(__name__)
class RealtimeManager:
"""实时数据管理器(单例)"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.subscribers: Dict[str, Set[WebSocket]] = {}
self.code_callbacks: Dict[str, List[Callable]] = {}
self._adapter = None
self._initialized = True
self._lock = asyncio.Lock()
async def subscribe(self, websocket: WebSocket, codes: List[str]):
"""
客户端订阅实时数据
Args:
websocket: WebSocket连接
codes: 代码列表
"""
await websocket.accept()
async with self._lock:
for code in codes:
if code not in self.subscribers:
self.subscribers[code] = set()
# 启动SDK订阅
await self._start_sdk_subscription(code)
self.subscribers[code].add(websocket)
logger.info(f"WebSocket {id(websocket)} 订阅了: {codes}")
async def unsubscribe(self, websocket: WebSocket, codes: List[str] = None):
"""
取消订阅
Args:
websocket: WebSocket连接
codes: 代码列表None表示取消所有
"""
async with self._lock:
codes_to_remove = codes if codes else list(self.subscribers.keys())
for code in codes_to_remove:
if code in self.subscribers:
self.subscribers[code].discard(websocket)
# 如果没有订阅者了取消SDK订阅
if not self.subscribers[code]:
del self.subscribers[code]
await self._stop_sdk_subscription(code)
logger.info(f"WebSocket {id(websocket)} 取消订阅")
async def _start_sdk_subscription(self, code: str):
"""启动SDK订阅"""
# 这里需要实现实际的SDK订阅逻辑
# 由于SDK的实时订阅是同步的回调需要在后台线程中运行
logger.info(f"开始SDK订阅: {code}")
async def _stop_sdk_subscription(self, code: str):
"""停止SDK订阅"""
logger.info(f"停止SDK订阅: {code}")
def on_sdk_data(self, code: str, data: dict):
"""
SDK数据回调
Args:
code: 代码
data: 数据字典
"""
# 保存到数据库
# self._save_snapshot(code, data)
# 推送给所有订阅者
if code in self.subscribers:
message = {
"type": "snapshot",
"code": code,
"data": data,
"timestamp": datetime.utcnow().isoformat()
}
# 异步推送
for ws in self.subscribers[code]:
asyncio.create_task(self._send_to_ws(ws, message))
async def _send_to_ws(self, websocket: WebSocket, message: dict):
"""发送消息到WebSocket"""
try:
await websocket.send_json(message)
except Exception as e:
logger.error(f"发送WebSocket消息失败: {str(e)}")
# 从订阅列表中移除
await self.unsubscribe(websocket)
def _save_snapshot(self, db: Session, code: str, data: dict):
"""保存快照到数据库"""
try:
expires_at = datetime.utcnow() + timedelta(days=settings.CACHE_AUTO_CLEANUP_DAYS)
snapshot = RealtimeSnapshot(
code=code,
security_type=data.get("security_type", "stock"),
trade_time=datetime.fromisoformat(data.get("trade_time", datetime.utcnow().isoformat())),
pre_close=data.get("pre_close"),
last=data.get("last"),
open=data.get("open"),
high=data.get("high"),
low=data.get("low"),
close=data.get("close"),
volume=data.get("volume"),
amount=data.get("amount"),
expires_at=expires_at
)
db.add(snapshot)
db.commit()
except Exception as e:
logger.error(f"保存快照失败: {str(e)}")
# 全局实时数据管理器实例
realtime_manager = RealtimeManager()
class RealtimeService:
"""实时数据服务"""
def __init__(self, db: Session):
self.db = db
self.base_service = BaseDataService(db)
self.manager = realtime_manager
def get_latest_snapshot(self, codes: List[str]) -> Dict[str, dict]:
"""
获取最新快照数据
Args:
codes: 代码列表
Returns:
快照数据字典
"""
result = {}
for code in codes:
# 查询最新的快照
snapshot = self.db.query(RealtimeSnapshot).filter(
RealtimeSnapshot.code == code
).order_by(RealtimeSnapshot.trade_time.desc()).first()
if snapshot:
result[code] = {
"code": snapshot.code,
"trade_time": snapshot.trade_time.isoformat(),
"pre_close": float(snapshot.pre_close) if snapshot.pre_close else None,
"last": float(snapshot.last) if snapshot.last else None,
"open": float(snapshot.open) if snapshot.open else None,
"high": float(snapshot.high) if snapshot.high else None,
"low": float(snapshot.low) if snapshot.low else None,
"close": float(snapshot.close) if snapshot.close else None,
"volume": int(snapshot.volume) if snapshot.volume else None,
"amount": float(snapshot.amount) if snapshot.amount else None
}
else:
result[code] = None
return result
async def subscribe_websocket(self, websocket: WebSocket, codes: List[str]):
"""订阅WebSocket"""
await self.manager.subscribe(websocket, codes)
async def unsubscribe_websocket(self, websocket: WebSocket, codes: List[str] = None):
"""取消WebSocket订阅"""
await self.manager.unsubscribe(websocket, codes)

@ -0,0 +1,154 @@
"""
SDK连接管理器 - 保持SDK登录状态避免重复登录
"""
import threading
import time
from typing import Optional, Dict, Any
from app.services.amazing_data_adapter import AmazingDataAdapter
from app.models.config import SDKConfig
from app.db.session import SessionLocal
import logging
logger = logging.getLogger(__name__)
class SDKConnectionManager:
"""
SDK连接管理器
- 保持SDK登录状态避免重复登录
- 登录成功后在释放前所有请求都不用重复登录
- 支持多配置管理
"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._connections: Dict[int, AmazingDataAdapter] = {}
cls._instance._connection_locks: Dict[int, threading.Lock] = {}
cls._instance._last_activity: Dict[int, float] = {}
return cls._instance
def get_connection(self, config_id: int) -> Optional[AmazingDataAdapter]:
"""
获取SDK连接
Args:
config_id: 配置ID
Returns:
AmazingDataAdapter实例如果连接失败返回None
"""
if config_id not in self._connection_locks:
self._connection_locks[config_id] = threading.Lock()
with self._connection_locks[config_id]:
if config_id in self._connections:
adapter = self._connections[config_id]
if adapter.is_connected():
self._last_activity[config_id] = time.time()
return adapter
else:
del self._connections[config_id]
db = SessionLocal()
try:
config = db.query(SDKConfig).filter(SDKConfig.id == config_id).first()
if not config:
logger.warning(f"SDK配置不存在: {config_id}")
return None
adapter = AmazingDataAdapter({
"username": config.username,
"password": config.password,
"host": config.host,
"port": config.port,
"local_path": config.local_path or "./amazing_data_cache/"
})
if adapter.connect():
self._connections[config_id] = adapter
self._last_activity[config_id] = time.time()
logger.info(f"SDK连接成功: config_id={config_id}")
return adapter
else:
logger.warning(f"SDK连接失败: config_id={config_id}")
return None
finally:
db.close()
def release_connection(self, config_id: int):
"""
释放SDK连接
Args:
config_id: 配置ID
"""
if config_id not in self._connection_locks:
return
with self._connection_locks[config_id]:
if config_id in self._connections:
adapter = self._connections[config_id]
try:
adapter.disconnect()
logger.info(f"SDK连接已释放: config_id={config_id}")
except Exception as e:
logger.warning(f"释放SDK连接时出错: {e}")
del self._connections[config_id]
if config_id in self._last_activity:
del self._last_activity[config_id]
def get_default_connection(self) -> Optional[AmazingDataAdapter]:
"""
获取默认SDK连接
Returns:
AmazingDataAdapter实例
"""
db = SessionLocal()
try:
config = db.query(SDKConfig).filter(SDKConfig.is_default == True).first()
if not config:
config = db.query(SDKConfig).first()
if config:
return self.get_connection(config.id)
return None
finally:
db.close()
def release_all(self):
"""释放所有SDK连接"""
for config_id in list(self._connections.keys()):
self.release_connection(config_id)
def get_status(self, config_id: int) -> Dict[str, Any]:
"""
获取连接状态
Args:
config_id: 配置ID
Returns:
状态信息
"""
if config_id in self._connections:
adapter = self._connections[config_id]
return {
"connected": adapter.is_connected(),
"last_activity": self._last_activity.get(config_id, 0),
"config_id": config_id
}
return {
"connected": False,
"last_activity": 0,
"config_id": config_id
}
sdk_manager = SDKConnectionManager()

@ -0,0 +1,348 @@
"""
股票数据服务
"""
from typing import List, Optional, Dict
from datetime import date
from sqlalchemy.orm import Session
from sqlalchemy import and_
import pandas as pd
import logging
from app.models.stock import StockKlineDaily, StockKlineMin
from app.services.sdk_manager import sdk_manager
from app.services.base_data_service import BaseDataService
from app.utils.date_utils import parse_date, format_date, get_market_from_code
from app.utils.data_utils import dataframe_to_dict_list, merge_kline_data
logger = logging.getLogger(__name__)
class StockService:
"""股票数据服务"""
def __init__(self, db: Session):
self.db = db
self.base_service = BaseDataService(db)
def _get_adapter(self):
"""获取SDK适配器使用连接管理器"""
return sdk_manager.get_default_connection()
def get_kline(
self,
codes: List[str],
start_date: date,
end_date: date,
period: str = "daily"
) -> Dict[str, List[dict]]:
"""
获取股票K线数据带缓存
Args:
codes: 代码列表
start_date: 开始日期
end_date: 结束日期
period: 周期 (daily, min1, min5, min15, min30, min60)
Returns:
字典 {code: [kline_data]}
"""
result = {}
for code in codes:
try:
if period == "daily":
data = self._get_daily_kline_with_cache(code, start_date, end_date)
else:
data = self._get_min_kline_with_cache(code, start_date, end_date, period)
result[code] = data
except Exception as e:
logger.error(f"获取{code}的K线数据失败: {str(e)}")
result[code] = []
return result
def _get_daily_kline_with_cache(
self,
code: str,
start_date: date,
end_date: date
) -> List[dict]:
"""获取日线数据(带缓存)"""
# 1. 查询本地缓存
cached_records = self.db.query(StockKlineDaily).filter(
and_(
StockKlineDaily.code == code,
StockKlineDaily.trade_date >= start_date,
StockKlineDaily.trade_date <= end_date
)
).order_by(StockKlineDaily.trade_date).all()
# 2. 检查数据完整性
cached_dates = {r.trade_date for r in cached_records}
expected_dates = set(self.base_service.get_trading_calendar(
get_market_from_code(code),
start_date,
end_date
))
missing_dates = expected_dates - cached_dates
# 3. 如果有缺失从SDK获取
if missing_dates:
try:
adapter = self._get_adapter()
if adapter:
sdk_data = adapter.get_kline([code], start_date, end_date, "daily")
if code in sdk_data and not sdk_data[code].empty:
# 保存到数据库
self._save_daily_kline(code, sdk_data[code])
# 重新查询
cached_records = self.db.query(StockKlineDaily).filter(
and_(
StockKlineDaily.code == code,
StockKlineDaily.trade_date >= start_date,
StockKlineDaily.trade_date <= end_date
)
).order_by(StockKlineDaily.trade_date).all()
except Exception as e:
logger.error(f"从SDK获取{code}数据失败: {str(e)}")
# 4. 转换为字典列表
return [
{
"trade_date": format_date(r.trade_date),
"open": float(r.open),
"high": float(r.high),
"low": float(r.low),
"close": float(r.close),
"volume": int(r.volume),
"amount": float(r.amount)
}
for r in cached_records
]
def _get_min_kline_with_cache(
self,
code: str,
start_date: date,
end_date: date,
period: str
) -> List[dict]:
"""获取分钟线数据(带缓存)"""
from datetime import datetime
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
# 1. 查询本地缓存
cached_records = self.db.query(StockKlineMin).filter(
and_(
StockKlineMin.code == code,
StockKlineMin.period_type == period,
StockKlineMin.trade_datetime >= start_datetime,
StockKlineMin.trade_datetime <= end_datetime
)
).order_by(StockKlineMin.trade_datetime).all()
# 2. 如果数据较少尝试从SDK获取
if len(cached_records) < 10:
try:
adapter = self._get_adapter()
if adapter:
sdk_data = adapter.get_kline([code], start_date, end_date, period)
if code in sdk_data and not sdk_data[code].empty:
self._save_min_kline(code, sdk_data[code], period)
# 重新查询
cached_records = self.db.query(StockKlineMin).filter(
and_(
StockKlineMin.code == code,
StockKlineMin.period_type == period,
StockKlineMin.trade_datetime >= start_datetime,
StockKlineMin.trade_datetime <= end_datetime
)
).order_by(StockKlineMin.trade_datetime).all()
except Exception as e:
logger.error(f"从SDK获取{code}分钟数据失败: {str(e)}")
return [
{
"trade_datetime": r.trade_datetime.isoformat(),
"open": float(r.open),
"high": float(r.high),
"low": float(r.low),
"close": float(r.close),
"volume": int(r.volume),
"amount": float(r.amount)
}
for r in cached_records
]
def _save_daily_kline(self, code: str, df: pd.DataFrame):
"""保存日线数据到数据库"""
if df.empty:
return
for idx, row in df.iterrows():
kline_time = row.get("kline_time")
if kline_time is None:
continue
trade_date = kline_time.date() if hasattr(kline_time, 'date') else parse_date(str(kline_time)[:10])
existing = self.db.query(StockKlineDaily).filter(
and_(
StockKlineDaily.code == code,
StockKlineDaily.trade_date == trade_date
)
).first()
if existing:
existing.open = float(row.get("open", 0))
existing.high = float(row.get("high", 0))
existing.low = float(row.get("low", 0))
existing.close = float(row.get("close", 0))
existing.volume = int(row.get("volume", 0))
existing.amount = float(row.get("amount", 0))
else:
record = StockKlineDaily(
code=code,
trade_date=trade_date,
open=float(row.get("open", 0)),
high=float(row.get("high", 0)),
low=float(row.get("low", 0)),
close=float(row.get("close", 0)),
volume=int(row.get("volume", 0)),
amount=float(row.get("amount", 0))
)
self.db.add(record)
self.db.commit()
def _save_min_kline(self, code: str, df: pd.DataFrame, period: str):
"""保存分钟线数据到数据库"""
if df.empty:
return
from datetime import datetime
for idx, row in df.iterrows():
kline_time = row.get("kline_time")
if kline_time is None:
continue
trade_datetime = kline_time if isinstance(kline_time, datetime) else datetime.fromisoformat(str(kline_time))
existing = self.db.query(StockKlineMin).filter(
and_(
StockKlineMin.code == code,
StockKlineMin.period_type == period,
StockKlineMin.trade_datetime == trade_datetime
)
).first()
if not existing:
record = StockKlineMin(
code=code,
period_type=period,
trade_datetime=trade_datetime,
open=float(row.get("open", 0)),
high=float(row.get("high", 0)),
low=float(row.get("low", 0)),
close=float(row.get("close", 0)),
volume=int(row.get("volume", 0)),
amount=float(row.get("amount", 0))
)
self.db.add(record)
self.db.commit()
def get_kline_chart_data(
self,
code: str,
start_date: date,
end_date: date,
period: str = "daily"
) -> dict:
"""
获取K线图数据ECharts格式
Returns:
{
"categoryData": ["2024-01-02", ...],
"values": [[open, close, low, high, volume], ...],
"volumes": [[index, volume, sign], ...]
}
"""
kline_data = self.get_kline([code], start_date, end_date, period)
data = kline_data.get(code, [])
if not data:
return {
"categoryData": [],
"values": [],
"volumes": []
}
category_data = []
values = []
volumes = []
for i, item in enumerate(data):
date_key = item.get("trade_date") or item.get("trade_datetime", "")[:10]
category_data.append(date_key)
# ECharts candlestick format: [open, close, low, high]
values.append([
item["open"],
item["close"],
item["low"],
item["high"],
item["volume"]
])
# Volume with color sign
sign = 1 if item["close"] >= item["open"] else -1
volumes.append([i, item["volume"], sign])
return {
"categoryData": category_data,
"values": values,
"volumes": volumes
}
def get_cache_status(self, code: str, period: str = "daily") -> dict:
"""获取代码缓存状态"""
if period == "daily":
query = self.db.query(StockKlineDaily).filter(StockKlineDaily.code == code)
count = query.count()
min_date = query.order_by(StockKlineDaily.trade_date).first()
max_date = query.order_by(StockKlineDaily.trade_date.desc()).first()
return {
"code": code,
"security_type": "stock",
"period_type": period,
"record_count": count,
"min_date": format_date(min_date.trade_date) if min_date else None,
"max_date": format_date(max_date.trade_date) if max_date else None
}
else:
query = self.db.query(StockKlineMin).filter(
StockKlineMin.code == code,
StockKlineMin.period_type == period
)
count = query.count()
return {
"code": code,
"security_type": "stock",
"period_type": period,
"record_count": count,
"min_date": None,
"max_date": None
}

@ -0,0 +1,268 @@
"""
测试中心服务
"""
import time
import logging
from typing import List, Dict, Optional
from datetime import date, datetime, timedelta
from sqlalchemy.orm import Session
from app.models.test import APITestLog
from app.services.base_data_service import BaseDataService
from app.services.stock_service import StockService
from app.services.future_service import FutureService
from app.services.finance_service import FinanceService
logger = logging.getLogger(__name__)
class TestService:
"""测试服务"""
# 测试端点定义
TEST_ENDPOINTS = [
# 基础数据
{"category": "base_data", "name": "获取代码列表", "endpoint": "/api/v1/base/codes", "method": "GET", "params": {"security_type": "EXTRA_STOCK_A"}},
{"category": "base_data", "name": "获取交易日历", "endpoint": "/api/v1/base/calendar", "method": "GET", "params": {"market": "SH", "start_date": "20240101", "end_date": "20241231"}},
# 股票数据
{"category": "stock", "name": "获取股票K线", "endpoint": "/api/v1/stock/kline", "method": "GET", "params": {"codes": "000001.SZ", "start_date": "20240101", "end_date": "20241231", "period": "daily"}},
{"category": "stock", "name": "获取股票K线图", "endpoint": "/api/v1/stock/kline/000001.SZ/chart", "method": "GET", "params": {"start_date": "20240101", "end_date": "20241231", "period": "daily"}},
# 期货数据
{"category": "future", "name": "获取期货K线", "endpoint": "/api/v1/future/kline", "method": "GET", "params": {"codes": "IF2501.CFE", "start_date": "20240101", "end_date": "20241231", "period": "daily"}},
{"category": "future", "name": "获取期货K线图", "endpoint": "/api/v1/future/kline/IF2501.CFE/chart", "method": "GET", "params": {"start_date": "20240101", "end_date": "20241231", "period": "daily"}},
# 财务数据
{"category": "finance", "name": "获取资产负债表", "endpoint": "/api/v1/finance/balance-sheet", "method": "GET", "params": {"codes": "000001.SZ", "start_date": "20240930", "end_date": "20240930"}},
{"category": "finance", "name": "获取现金流量表", "endpoint": "/api/v1/finance/cash-flow", "method": "GET", "params": {"codes": "000001.SZ", "start_date": "20240930", "end_date": "20240930"}},
{"category": "finance", "name": "获取利润表", "endpoint": "/api/v1/finance/income", "method": "GET", "params": {"codes": "000001.SZ", "start_date": "20240930", "end_date": "20240930"}},
# 实时数据
{"category": "realtime", "name": "获取最新快照", "endpoint": "/api/v1/realtime/snapshot", "method": "GET", "params": {"codes": "000001.SZ"}},
# 缓存管理
{"category": "cache", "name": "获取缓存任务列表", "endpoint": "/api/v1/cache/tasks", "method": "GET", "params": {"page": 1, "page_size": 20}},
]
def __init__(self, db: Session):
self.db = db
self.base_service = BaseDataService(db)
self.stock_service = StockService(db)
self.future_service = FutureService(db)
self.finance_service = FinanceService(db)
def get_categories(self) -> List[Dict]:
"""获取测试分类"""
categories = set(endpoint["category"] for endpoint in self.TEST_ENDPOINTS)
category_names = {
"base_data": "基础数据",
"stock": "股票数据",
"future": "期货数据",
"realtime": "实时数据",
"finance": "财务数据",
"shareholder": "股东数据",
"margin": "融资融券",
"index": "指数数据",
"etf": "ETF数据",
"kzz": "可转债数据",
"cache": "缓存管理"
}
return [
{"key": cat, "name": category_names.get(cat, cat)}
for cat in sorted(categories)
]
def get_endpoints(self, category: str = None) -> List[Dict]:
"""获取测试端点列表"""
if category:
return [ep for ep in self.TEST_ENDPOINTS if ep["category"] == category]
return self.TEST_ENDPOINTS
def run_test(self, endpoint: str, method: str, params: dict) -> Dict:
"""
运行单个测试
Args:
endpoint: 端点路径
method: HTTP方法
params: 请求参数
Returns:
测试结果
"""
start_time = time.time()
try:
# 根据端点调用相应的服务方法
result = self._call_endpoint(endpoint, method, params)
execution_time = int((time.time() - start_time) * 1000)
return {
"success": True,
"endpoint": endpoint,
"method": method,
"status_code": 200,
"execution_time_ms": execution_time,
"response_data": result,
"error_message": None
}
except Exception as e:
execution_time = int((time.time() - start_time) * 1000)
logger.error(f"测试失败 {endpoint}: {str(e)}")
return {
"success": False,
"endpoint": endpoint,
"method": method,
"status_code": 500,
"execution_time_ms": execution_time,
"response_data": None,
"error_message": str(e)
}
def _call_endpoint(self, endpoint: str, method: str, params: dict):
"""调用端点对应的服务方法"""
from app.utils.date_utils import parse_date
# 股票数据接口
if "/stock/kline" in endpoint and "/chart" in endpoint:
code = endpoint.split("/")[-2]
start_date = parse_date(params.get("start_date", "20240101"))
end_date = parse_date(params.get("end_date", "20241231"))
period = params.get("period", "daily")
return self.stock_service.get_kline_chart_data(code, start_date, end_date, period)
elif "/stock/kline" in endpoint:
codes = params.get("codes", "").split(",")
start_date = parse_date(params.get("start_date", "20240101"))
end_date = parse_date(params.get("end_date", "20241231"))
period = params.get("period", "daily")
return self.stock_service.get_kline(codes, start_date, end_date, period)
# 期货数据接口
elif "/future/kline" in endpoint and "/chart" in endpoint:
code = endpoint.split("/")[-2]
start_date = parse_date(params.get("start_date", "20240101"))
end_date = parse_date(params.get("end_date", "20241231"))
period = params.get("period", "daily")
return self.future_service.get_kline_chart_data(code, start_date, end_date, period)
elif "/future/kline" in endpoint:
codes = params.get("codes", "").split(",")
start_date = parse_date(params.get("start_date", "20240101"))
end_date = parse_date(params.get("end_date", "20241231"))
period = params.get("period", "daily")
return self.future_service.get_kline(codes, start_date, end_date, period)
# 财务数据接口
elif "/finance/balance-sheet" in endpoint:
codes = params.get("codes", "").split(",")
start_date = parse_date(params.get("start_date", "20240930"))
end_date = parse_date(params.get("end_date", "20240930"))
return self.finance_service.get_balance_sheet(codes, start_date, end_date)
elif "/finance/cash-flow" in endpoint:
codes = params.get("codes", "").split(",")
start_date = parse_date(params.get("start_date", "20240930"))
end_date = parse_date(params.get("end_date", "20240930"))
return self.finance_service.get_cash_flow(codes, start_date, end_date)
elif "/finance/income" in endpoint:
codes = params.get("codes", "").split(",")
start_date = parse_date(params.get("start_date", "20240930"))
end_date = parse_date(params.get("end_date", "20240930"))
return self.finance_service.get_income_statement(codes, start_date, end_date)
# 基础数据接口
elif "/base/codes" in endpoint:
security_type = params.get("security_type", "EXTRA_STOCK_A")
return {"codes": self.base_service.get_code_list(security_type)[:10]}
elif "/base/calendar" in endpoint:
market = params.get("market", "SH")
start_date = parse_date(params.get("start_date", "20240101"))
end_date = parse_date(params.get("end_date", "20241231"))
calendar = self.base_service.get_trading_calendar(market, start_date, end_date)
return {"calendar": [d.isoformat() for d in calendar[:10]]}
# 其他接口
else:
return {"message": "测试通过"}
def run_all_tests(self, categories: List[str] = None) -> Dict:
"""
运行全部测试
Args:
categories: 测试分类列表None表示全部
Returns:
测试结果汇总
"""
endpoints = self.get_endpoints()
if categories:
endpoints = [ep for ep in endpoints if ep["category"] in categories]
results = []
passed = 0
failed = 0
for endpoint in endpoints:
result = self.run_test(
endpoint["endpoint"],
endpoint["method"],
endpoint.get("params", {})
)
results.append(result)
if result["success"]:
passed += 1
else:
failed += 1
return {
"total": len(results),
"passed": passed,
"failed": failed,
"results": results
}
def log_test(self, test_name: str, api_category: str, api_endpoint: str,
request_method: str, request_params: dict, response_data: dict,
status_code: int, execution_time_ms: int, is_success: bool,
error_message: str = None):
"""记录测试日志"""
log = APITestLog(
test_name=test_name,
api_category=api_category,
api_endpoint=api_endpoint,
request_method=request_method,
request_params=request_params,
response_data=response_data,
status_code=status_code,
execution_time_ms=execution_time_ms,
is_success=is_success,
error_message=error_message
)
self.db.add(log)
self.db.commit()
def get_test_history(self, page: int = 1, page_size: int = 20) -> Dict:
"""获取测试历史"""
query = self.db.query(APITestLog).order_by(APITestLog.created_at.desc())
total = query.count()
logs = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"items": logs,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size
}

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AmazingData金融数据服务平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 600px;
width: 90%;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 10px;
}
.subtitle {
color: #666;
text-align: center;
margin-bottom: 30px;
}
.status {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.status-title {
font-weight: bold;
color: #0369a1;
margin-bottom: 10px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #e0f2fe;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
color: #64748b;
}
.status-value {
color: #0ea5e9;
font-weight: 500;
}
.features {
margin-top: 20px;
}
.features h3 {
color: #333;
margin-bottom: 15px;
}
.feature-list {
list-style: none;
}
.feature-list li {
padding: 8px 0;
color: #555;
display: flex;
align-items: center;
}
.feature-list li::before {
content: "✓";
color: #22c55e;
font-weight: bold;
margin-right: 10px;
}
.api-link {
display: block;
text-align: center;
margin-top: 20px;
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
transition: transform 0.2s;
}
.api-link:hover {
transform: translateY(-2px);
}
.footer {
text-align: center;
margin-top: 20px;
color: #94a3b8;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>AmazingData</h1>
<p class="subtitle">金融数据服务平台</p>
<div class="status">
<div class="status-title">系统状态</div>
<div class="status-item">
<span class="status-label">后端服务</span>
<span class="status-value" id="backend-status">检查中...</span>
</div>
<div class="status-item">
<span class="status-label">API版本</span>
<span class="status-value" id="api-version">-</span>
</div>
<div class="status-item">
<span class="status-label">数据库</span>
<span class="status-value">SQLite (演示模式)</span>
</div>
</div>
<div class="features">
<h3>功能特性</h3>
<ul class="feature-list">
<li>完整SDK接口封装</li>
<li>智能数据缓存</li>
<li>实时数据订阅</li>
<li>缺失数据检测</li>
<li>批量缓存管理</li>
<li>可视化K线图</li>
<li>完整测试中心</li>
</ul>
</div>
<a href="/docs" class="api-link">查看API文档</a>
<div class="footer">
默认账号: admin / admin123
</div>
</div>
<script>
// 检查后端状态
fetch('/api/v1/health')
.then(res => res.json())
.then(data => {
document.getElementById('backend-status').textContent = '运行中 ✓';
document.getElementById('backend-status').style.color = '#22c55e';
document.getElementById('api-version').textContent = data.version;
})
.catch(() => {
document.getElementById('backend-status').textContent = '未连接 ✗';
document.getElementById('backend-status').style.color = '#ef4444';
});
</script>
</body>
</html>

@ -0,0 +1,14 @@
# 工具模块
from app.utils.date_utils import parse_date, format_date, get_trading_days_between
from app.utils.data_utils import deduplicate_dataframe, compare_kline_data
from app.utils.validators import validate_code_format, validate_date_range
__all__ = [
"parse_date",
"format_date",
"get_trading_days_between",
"deduplicate_dataframe",
"compare_kline_data",
"validate_code_format",
"validate_date_range",
]

@ -0,0 +1,141 @@
"""
数据处理工具模块
"""
from typing import List, Dict, Tuple, Optional
import pandas as pd
import numpy as np
def deduplicate_dataframe(
df: pd.DataFrame,
existing_df: pd.DataFrame,
core_columns: List[str] = None,
tolerance: float = 0.0001
) -> pd.DataFrame:
"""
去重DataFrame
Args:
df: 新数据
existing_df: 已存在的数据
core_columns: 核心比较列
tolerance: 差异容忍度
Returns:
需要去重的数据
"""
if core_columns is None:
core_columns = ["open", "high", "low", "close", "volume"]
if existing_df.empty:
return df
# 获取已存在的索引
existing_indices = set(existing_df.index)
to_keep = []
for idx, row in df.iterrows():
if idx not in existing_indices:
to_keep.append(idx)
else:
# 比较核心字段
existing_row = existing_df.loc[idx]
is_same = all(
abs(row[col] - existing_row[col]) < tolerance
for col in core_columns
if col in row and col in existing_row
)
if not is_same:
to_keep.append(idx)
return df.loc[to_keep]
def compare_kline_data(
new_data: Dict,
existing_data: Dict,
tolerance: float = 0.0001
) -> bool:
"""
比较K线数据是否相同
Args:
new_data: 新数据
existing_data: 已存在数据
tolerance: 差异容忍度
Returns:
是否相同
"""
core_fields = ["open", "high", "low", "close", "volume"]
for field in core_fields:
if field not in new_data or field not in existing_data:
return False
if abs(new_data[field] - existing_data[field]) >= tolerance:
return False
return True
def dataframe_to_dict_list(df: pd.DataFrame) -> List[Dict]:
"""将DataFrame转换为字典列表"""
if df.empty:
return []
# 重置索引以便包含日期信息
df_reset = df.reset_index()
# 转换列名
df_reset.columns = [str(col).lower() for col in df_reset.columns]
# 转换为字典列表
records = df_reset.to_dict("records")
# 处理数值类型
for record in records:
for key, value in record.items():
if isinstance(value, (np.integer, np.floating)):
record[key] = float(value) if isinstance(value, np.floating) else int(value)
elif pd.isna(value):
record[key] = None
return records
def merge_kline_data(
cached_data: pd.DataFrame,
sdk_data: pd.DataFrame
) -> pd.DataFrame:
"""合并K线数据SDK数据优先"""
if cached_data.empty:
return sdk_data
if sdk_data.empty:
return cached_data
# 合并数据SDK数据优先
combined = pd.concat([cached_data, sdk_data])
combined = combined[~combined.index.duplicated(keep="last")]
combined = combined.sort_index()
return combined
def calculate_data_completeness(
actual_count: int,
expected_count: int
) -> float:
"""计算数据完整度"""
if expected_count == 0:
return 1.0
return actual_count / expected_count
def detect_missing_periods(
existing_dates: List,
expected_dates: List
) -> List[Tuple]:
"""检测缺失的时间段"""
missing = set(expected_dates) - set(existing_dates)
return sorted(list(missing))

@ -0,0 +1,76 @@
"""
日期工具模块
"""
from datetime import date, datetime, timedelta
from typing import List, Optional
def parse_date(date_str: str) -> date:
"""解析日期字符串 (YYYYMMDD 或 YYYY-MM-DD)"""
if len(date_str) == 8:
return datetime.strptime(date_str, "%Y%m%d").date()
elif len(date_str) == 10:
return datetime.strptime(date_str, "%Y-%m-%d").date()
else:
raise ValueError(f"无效的日期格式: {date_str}")
def format_date(d: date, format_str: str = "%Y-%m-%d") -> str:
"""格式化日期"""
return d.strftime(format_str)
def format_datetime(dt: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
"""格式化日期时间"""
return dt.strftime(format_str)
def get_market_from_code(code: str) -> str:
"""从代码获取市场"""
if code.endswith(".SH"):
return "SH"
elif code.endswith(".SZ"):
return "SZ"
elif code.endswith(".BJ"):
return "BJ"
elif code.endswith(".CFE"):
return "CFE"
else:
return "SH" # 默认上海
def get_trading_days_between(
start_date: date,
end_date: date,
calendar: Optional[List[date]] = None
) -> List[date]:
"""获取两个日期之间的交易日列表"""
if calendar:
return [d for d in calendar if start_date <= d <= end_date]
# 如果没有提供交易日历,简单排除周末
trading_days = []
current = start_date
while current <= end_date:
if current.weekday() < 5: # 周一到周五
trading_days.append(current)
current += timedelta(days=1)
return trading_days
def get_default_date_range(days: int = 365) -> tuple:
"""获取默认日期范围"""
end_date = date.today()
start_date = end_date - timedelta(days=days)
return start_date, end_date
def int_to_date(date_int: int) -> date:
"""将整数日期(YYYYMMDD)转换为date对象"""
return datetime.strptime(str(date_int), "%Y%m%d").date()
def date_to_int(d: date) -> int:
"""将date对象转换为整数日期(YYYYMMDD)"""
return int(d.strftime("%Y%m%d"))

@ -0,0 +1,79 @@
"""
验证器模块
"""
import re
from datetime import date
from typing import Tuple, Optional
def validate_code_format(code: str) -> Tuple[bool, str]:
"""
验证代码格式
Returns:
(是否有效, 错误信息)
"""
if not code:
return False, "代码不能为空"
# 股票代码格式: 000001.SZ, 600000.SH
stock_pattern = r"^\d{6}\.(SZ|SH|BJ)$"
# 期货代码格式: IF2501.CFE
future_pattern = r"^[A-Z]{1,2}\d{4}\.CFE$"
if re.match(stock_pattern, code):
return True, ""
if re.match(future_pattern, code):
return True, ""
return False, f"无效的代码格式: {code}"
def validate_date_range(start_date: date, end_date: date) -> Tuple[bool, str]:
"""
验证日期范围
Returns:
(是否有效, 错误信息)
"""
if start_date > end_date:
return False, "开始日期不能晚于结束日期"
# 检查日期范围是否合理不超过5年
from datetime import timedelta
if (end_date - start_date).days > 365 * 5:
return False, "日期范围不能超过5年"
return True, ""
def validate_period_type(period: str) -> Tuple[bool, str]:
"""
验证周期类型
Returns:
(是否有效, 错误信息)
"""
valid_periods = ["daily", "min1", "min5", "min15", "min30", "min60"]
if period not in valid_periods:
return False, f"无效的周期类型: {period},有效值: {', '.join(valid_periods)}"
return True, ""
def validate_security_type(security_type: str) -> Tuple[bool, str]:
"""
验证证券类型
Returns:
(是否有效, 错误信息)
"""
valid_types = ["stock", "future", "index", "etf", "kzz"]
if security_type not in valid_types:
return False, f"无效的证券类型: {security_type},有效值: {', '.join(valid_types)}"
return True, ""

@ -0,0 +1,37 @@
# Web框架
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
websockets==12.0
# 数据库
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
aiosqlite==0.19.0
alembic==1.12.1
redis==5.0.1
# 认证
pyjwt==2.8.0
bcrypt==4.1.1
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
# 数据处理
pandas==2.1.3
numpy==1.26.2
# 任务调度
apscheduler==3.10.4
# 配置管理
pydantic==2.5.0
pydantic-settings==2.1.0
python-dotenv==1.0.0
# 工具
python-dateutil==2.8.2
httpx==0.25.2
# AmazingData SDK (本地安装)
# AmazingData-1.0.24-py3-none-any.whl

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AmazingData金融数据服务平台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 600px;
width: 90%;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 10px;
}
.subtitle {
color: #666;
text-align: center;
margin-bottom: 30px;
}
.status {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.status-title {
font-weight: bold;
color: #0369a1;
margin-bottom: 10px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #e0f2fe;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
color: #64748b;
}
.status-value {
color: #0ea5e9;
font-weight: 500;
}
.features {
margin-top: 20px;
}
.features h3 {
color: #333;
margin-bottom: 15px;
}
.feature-list {
list-style: none;
}
.feature-list li {
padding: 8px 0;
color: #555;
display: flex;
align-items: center;
}
.feature-list li::before {
content: "✓";
color: #22c55e;
font-weight: bold;
margin-right: 10px;
}
.api-link {
display: block;
text-align: center;
margin-top: 20px;
padding: 12px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 6px;
transition: transform 0.2s;
}
.api-link:hover {
transform: translateY(-2px);
}
.footer {
text-align: center;
margin-top: 20px;
color: #94a3b8;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>AmazingData</h1>
<p class="subtitle">金融数据服务平台</p>
<div class="status">
<div class="status-title">系统状态</div>
<div class="status-item">
<span class="status-label">后端服务</span>
<span class="status-value" id="backend-status">检查中...</span>
</div>
<div class="status-item">
<span class="status-label">API版本</span>
<span class="status-value" id="api-version">-</span>
</div>
<div class="status-item">
<span class="status-label">数据库</span>
<span class="status-value">SQLite (演示模式)</span>
</div>
</div>
<div class="features">
<h3>功能特性</h3>
<ul class="feature-list">
<li>完整SDK接口封装</li>
<li>智能数据缓存</li>
<li>实时数据订阅</li>
<li>缺失数据检测</li>
<li>批量缓存管理</li>
<li>可视化K线图</li>
<li>完整测试中心</li>
</ul>
</div>
<a href="/docs" class="api-link">查看API文档</a>
<div class="footer">
默认账号: admin / admin123
</div>
</div>
<script>
// 检查后端状态
fetch('/api/v1/health')
.then(res => res.json())
.then(data => {
document.getElementById('backend-status').textContent = '运行中 ✓';
document.getElementById('backend-status').style.color = '#22c55e';
document.getElementById('api-version').textContent = data.version;
})
.catch(() => {
document.getElementById('backend-status').textContent = '未连接 ✗';
document.getElementById('backend-status').style.color = '#ef4444';
});
</script>
</body>
</html>

@ -0,0 +1,53 @@
"""
AmazingData SDK 连接测试脚本
"""
import sys
import time
def test_sdk_connection():
from app.db.session import SessionLocal
from app.models.config import SDKConfig
from app.services.amazing_data_adapter import AmazingDataAdapter
db = SessionLocal()
config = db.query(SDKConfig).filter(SDKConfig.id == 1).first()
if not config:
print("No SDK config found with id=1")
return False
print(f"Testing SDK connection with config:")
print(f" Username: {config.username}")
print(f" Host: {config.host}")
print(f" Port: {config.port}")
adapter = AmazingDataAdapter({
"username": config.username,
"password": config.password,
"host": config.host,
"port": config.port,
"local_path": config.local_path or "./amazing_data_cache/"
})
start = time.time()
print("\nConnecting to SDK...")
try:
success = adapter.connect()
elapsed = time.time() - start
if success:
print(f"✓ SDK connection successful! (took {elapsed:.2f}s)")
adapter.disconnect()
print("✓ Disconnected successfully")
return True
else:
print(f"✗ SDK connection failed! (took {elapsed:.2f}s)")
return False
except Exception as e:
print(f"✗ Error: {type(e).__name__}: {e}")
return False
if __name__ == "__main__":
success = test_sdk_connection()
sys.exit(0 if success else 1)

@ -0,0 +1,790 @@
# AmazingDataAdapter 接口文档
基于银河证券星耀数智量化平台 SDK 的 Python 适配器接口文档
---
## 目录
1. [快速开始](#快速开始)
2. [枚举类型](#枚举类型)
3. [接口列表](#接口列表)
- [基础数据接口](#基础数据接口)
- [历史行情接口](#历史行情接口)
- [财务数据接口](#财务数据接口)
- [股东股本接口](#股东股本接口)
- [融资融券接口](#融资融券接口)
- [交易异动接口](#交易异动接口)
- [指数数据接口](#指数数据接口)
- [ETF数据接口](#etf数据接口)
- [可转债数据接口](#可转债数据接口)
---
## 快速开始
```python
import asyncio
from app.adapters.amazingdata_adapter import AmazingDataAdapter, SecurityType, Market
async def main():
# 1. 创建适配器
adapter = AmazingDataAdapter()
# 2. 连接数据源
await adapter.connect({
"username": "your_username",
"password": "your_password",
"host": "your_host",
"port": 8600,
"local_path": "./amazing_data_cache/",
"use_local_cache": True
})
# 3. 获取数据
klines = await adapter.fetch_klines(
symbol="000001.SZ",
start="20240101",
end="20241231",
freq="1d"
)
print(f"获取到 {len(klines)} 条K线数据")
# 4. 断开连接
await adapter.close()
asyncio.run(main())
```
---
## 枚举类型
### SecurityType - 证券类型
| 枚举值 | 说明 |
|--------|------|
| `SecurityType.STOCK_A` | 沪深A股 |
| `SecurityType.STOCK_A_SH_SZ` | 沪深A股沪深 |
| `SecurityType.INDEX_A` | 沪深指数 |
| `SecurityType.ETF` | ETF |
| `SecurityType.FUTURE` | 期货 |
| `SecurityType.KZZ` | 可转债 |
| `SecurityType.GLRA` | 逆回购 |
| `SecurityType.HKT` | 港股通 |
| `SecurityType.ETF_OP` | ETF期权 |
### Market - 市场
| 枚举值 | 说明 |
|--------|------|
| `Market.SH` | 上海 |
| `Market.SZ` | 深圳 |
| `Market.BJ` | 北京 |
---
## 接口列表
### 基础数据接口
#### 1. fetch_klines - 获取历史K线
```python
async def fetch_klines(
self,
symbol: str,
start: str,
end: str,
freq: str
) -> List[KLineData]
```
**参数说明:**
| 参数 | 类型 | 说明 |
|------|------|------|
| symbol | str | 标的代码,如 "000001.SZ" |
| start | str | 开始日期,格式 YYYYMMDD |
| end | str | 结束日期,格式 YYYYMMDD |
| freq | str | 周期1m/5m/15m/30m/60m/1d/1w/1M |
**返回数据 (KLineData)**
| 字段 | 类型 | 说明 |
|------|------|------|
| symbol | str | 标的代码 |
| time | int | Unix时间戳 |
| open | float | 开盘价 |
| high | float | 最高价 |
| low | float | 最低价 |
| close | float | 收盘价 |
| volume | int | 成交量 |
| amount | float | 成交金额 |
| trade_date | str | 交易日 (YYYY-MM-DD) |
| is_limit_up | bool | 是否涨停 |
| is_limit_down | bool | 是否跌停 |
| total_market_cap | float | 总市值(元) |
| float_market_cap | float | 流通市值(元) |
| inst_holding_ratio | float | 机构持仓占比(% |
| trading_days | int | 可交易日数 |
---
#### 2. fetch_symbols - 获取标的列表
```python
async def fetch_symbols(
self,
asset_type: str
) -> List[SymbolInfo]
```
**参数说明:**
| 参数 | 类型 | 说明 |
|------|------|------|
| asset_type | str | 资产类型stock/futures |
**返回数据 (SymbolInfo)**
| 字段 | 类型 | 说明 |
|------|------|------|
| symbol_id | str | 标的代码 |
| name | str | 标的名称 |
| exchange | str | 交易所 |
| underlying | str | 期货品种代码 |
| contract_month | str | 合约月份 |
---
#### 3. fetch_trading_calendar - 获取交易日历
```python
async def fetch_trading_calendar(
self,
exchange: str,
start: str,
end: str
) -> List[TradeCalData]
```
**参数说明:**
| 参数 | 类型 | 说明 |
|------|------|------|
| exchange | str | 交易所代码SH/SZ |
| start | str | 开始日期 YYYYMMDD |
| end | str | 结束日期 YYYYMMDD |
---
#### 4. get_code_info - 获取证券基本信息
```python
async def get_code_info(
self,
security_type: SecurityType = SecurityType.STOCK_A
) -> pd.DataFrame
```
**返回字段:**
| 字段 | 说明 |
|------|------|
| symbol | 证券简称 |
| security_status | 产品状态标志 |
| pre_close | 昨收价 |
| high_limited | 涨停价 |
| low_limited | 跌停价 |
| price_tick | 最小价格变动单位 |
---
#### 5. get_trading_calendar - 获取交易日历(列表)
```python
async def get_trading_calendar(
self,
market: Market = Market.SH
) -> List[int]
```
**返回:** 交易日列表,格式 [20240102, 20240103, ...]
---
#### 6. get_adj_factor - 获取单次复权因子
```python
async def get_adj_factor(
self,
codes: List[str],
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**参数说明:**
| 参数 | 类型 | 说明 |
|------|------|------|
| codes | List[str] | 股票代码列表 |
| is_local | bool | 是否使用本地缓存 |
**返回:** DataFrame (index: 日期, columns: 股票代码)
---
#### 7. get_backward_factor - 获取后复权因子
```python
async def get_backward_factor(
self,
codes: List[str],
is_local: Optional[bool] = None
) -> pd.DataFrame
```
---
### 历史行情接口
#### 8. get_snapshot - 获取历史快照
```python
async def get_snapshot(
self,
codes: List[str],
start_date: str,
end_date: str
) -> Dict[str, pd.DataFrame]
```
**说明:** 获取Level-1行情快照数据
---
### 财务数据接口
#### 9. get_balance_sheet - 资产负债表
```python
async def get_balance_sheet(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| TOTAL_ASSETS | 资产总计 |
| TOTAL_CUR_ASSETS | 流动资产合计 |
| TOTAL_NONCUR_ASSETS | 非流动资产合计 |
| TOTAL_LIAB | 负债合计 |
| TOT_SHARE_EQUITY_INCL_MIN_INT | 股东权益合计 |
| CURRENCY_CAP | 货币资金 |
| NOTES_RECEIVABLE | 应收票据 |
| ACCT_RECEIVABLE | 应收账款 |
| INV | 存货 |
---
#### 10. get_cash_flow - 现金流量表
```python
async def get_cash_flow(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| NET_CASH_FLOWS_OPERA_ACT | 经营活动现金流净额 |
| NET_CASH_FLOWS_INV_ACT | 投资活动现金流净额 |
| NET_CASH_FLOWS_FIN_ACT | 筹资活动现金流净额 |
| NET_INCR_CASH_AND_CASH_EQU | 现金及现金等价物净增加额 |
---
#### 11. get_income_statement - 利润表
```python
async def get_income_statement(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| TOT_OPERA_REV | 营业总收入 |
| OPERA_REV | 营业收入 |
| TOT_OPERA_COST | 营业总成本 |
| OPERA_PROFIT | 营业利润 |
| TOTAL_PROFIT | 利润总额 |
| NET_PRO_INCL_MIN_INT_INC | 净利润 |
| BASIC_EPS | 基本每股收益 |
| DILUTED_EPS | 稀释每股收益 |
| RD_EXP | 研发费用 |
---
#### 12. get_profit_express - 业绩快报
```python
async def get_profit_express(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| TOTAL_ASSETS | 总资产 |
| NET_PRO_EXCL_MIN_INT_INC | 净利润 |
| TOT_OPERA_REV | 营业总收入 |
| TOTAL_PROFIT | 利润总额 |
| OPERA_PROFIT | 营业利润 |
| EPS_BASIC | 基本每股收益 |
| ROE_WEIGHTED | 净资产收益率-加权 |
| YOY_GR_NET_PROFIT_PARENT | 同比增长率 |
---
#### 13. get_profit_notice - 业绩预告
```python
async def get_profit_notice(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| P_TYPECODE | 业绩预告类型代码 |
| P_CHANGE_MAX | 预告净利润变动幅度上限 |
| P_CHANGE_MIN | 预告净利润变动幅度下限 |
| NET_PROFIT_MAX | 预告净利润上限(万元) |
| NET_PROFIT_MIN | 预告净利润下限(万元) |
| P_REASON | 业绩变动原因 |
---
### 股东股本接口
#### 14. get_top10_shareholders - 十大股东
```python
async def get_top10_shareholders(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| HOLDER_NAME | 股东名称 |
| HOLDER_QUANTITY | 持股数 |
| HOLDER_PCT | 持股比例(%) |
| HOLDER_HOLDER_CATEGORY | 股东性质(1:个人, 2:公司) |
| FLOAT_QTY | 流通股数量 |
---
#### 15. get_shareholder_count - 股东户数
```python
async def get_shareholder_count(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| HOLDER_TOTAL_NUM | A股、B股、H股、境外股的总户数 |
| HOLDER_NUM | A股股东户数 |
---
#### 16. get_equity_structure - 股本结构
```python
async def get_equity_structure(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| TOT_SHARE | 总股本(万股) |
| FLOAT_SHARE | 流通股(万股) |
| FLOAT_A_SHARE | 流通A股(万股) |
| RESTRICTED_A_SHARE | 限售A股(万股) |
| TOT_RESTRICTED_SHARE | 限售股合计 |
---
### 融资融券接口
#### 17. get_margin_summary - 融资融券汇总
```python
async def get_margin_summary(
self,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| TRADE_DATE | 交易日期 |
| SUM_BORROW_MONEY_BALANCE | 融资余额(元) |
| SUM_PURCH_WITH_BORROW_MONEY | 融资买入额(元) |
| SUM_REPAYMENT_OF_BORROW_MONEY | 融资偿还额(元) |
| SUM_SEC_LENDING_BALANCE | 融券余额(元) |
| SUM_SALES_OF_BORROWED_SEC | 融券卖出量 |
| SUM_MARGIN_TRADE_BALANCE | 融资融券余额(元) |
---
#### 18. get_margin_detail - 个股融资融券明细
```python
async def get_margin_detail(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| BORROW_MONEY_BALANCE | 融资余额 |
| PURCH_WITH_BORROW_MONEY | 融资买入额 |
| REPAYMENT_OF_BORROW_MONEY | 融资偿还额 |
| SEC_LENDING_BALANCE | 融券余额 |
| SALES_OF_BORROWED_SEC | 融券卖出量 |
---
### 交易异动接口
#### 19. get_longhu_bang - 龙虎榜数据
```python
async def get_longhu_bang(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| TRADE_DATE | 交易日期 |
| REASON_TYPE_NAME | 上榜原因 |
| CHANGE_RANGE | 涨跌幅(%) |
| TRADER_NAME | 营业部名称 |
| BUY_AMOUNT | 买入金额(元) |
| SELL_AMOUNT | 卖出金额(元) |
| FLOW_MARK | 买卖表示(1买入, 2卖出) |
---
#### 20. get_block_trading - 大宗交易
```python
async def get_block_trading(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> pd.DataFrame
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| TRADE_DATE | 交易日期 |
| B_SHARE_PRICE | 成交价(元) |
| B_SHARE_VOLUME | 成交量(万股) |
| B_SHARE_AMOUNT | 成交金额(万元) |
| B_BUYER_NAME | 买方营业部名称 |
| B_SELLER_NAME | 卖方营业部名称 |
---
### 指数数据接口
#### 21. get_index_constituents - 指数成分股
```python
async def get_index_constituents(
self,
codes: List[str],
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**支持指数:**
| 代码 | 名称 |
|------|------|
| 000016.SH | 上证50 |
| 000300.SH | 沪深300 |
| 000905.SH | 中证500 |
| 000906.SH | 中证800 |
| 000852.SH | 中证1000 |
**返回字段:**
| 字段 | 说明 |
|------|------|
| INDEX_CODE | 指数代码 |
| CON_CODE | 成分股代码 |
| INDATE | 纳入日期 |
| OUTDATE | 剔除日期 |
| INDEX_NAME | 指数名称 |
---
#### 22. get_index_weights - 成分股权重
```python
async def get_index_weights(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**返回字段:**
| 字段 | 说明 |
|------|------|
| INDEX_CODE | 指数代码 |
| CON_CODE | 标的代码 |
| TRADE_DATE | 生效日期 |
| WEIGHT | 权重(%) |
| CLOSE | 收盘价 |
---
### ETF数据接口
#### 23. get_etf_pcf - ETF申赎数据
```python
async def get_etf_pcf(
self,
codes: List[str]
) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]
```
**返回:** (etf_info, etf_constituents)
**etf_info字段**
| 字段 | 说明 |
|------|------|
| creation_redemption_unit | 每个篮子对应的ETF份数 |
| max_cash_ratio | 最大现金替代比例 |
| creation | 是否允许申购 |
| redemption | 是否允许赎回 |
**etf_constituents字段**
| 字段 | 说明 |
|------|------|
| underlying_symbol | 成份证券简称 |
| component_share | 成份证券数量 |
| substitute_flag | 现金替代标志 |
---
#### 24. get_fund_share - 基金份额
```python
async def get_fund_share(
self,
codes: List[str],
start_date: Optional[str] = None,
end_date: Optional[str] = None,
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| FUND_SHARE | 基金份额(万份) |
| TOTAL_SHARE | 基金总份额(万份) |
| FLOAT_SHARE | 流通份额(万份) |
| CHANGE_REASON | 份额变动原因 |
---
### 可转债数据接口
#### 25. get_kzz_issuance - 可转债发行数据
```python
async def get_kzz_issuance(
self,
codes: List[str],
is_local: Optional[bool] = None
) -> Dict[str, pd.DataFrame]
```
**主要字段:**
| 字段 | 说明 |
|------|------|
| STOCK_CODE | 正股代码 |
| LISTED_DATE | 上市日期 |
| PLAN_SCHEDULE | 方案进度 |
| CLAUSE_INI_CONV_PRICE | 初始转换价格 |
| LIST_ISSUE_SIZE | 发行规模(万元) |
| LIST_ISSUE_QUANTITY | 发行数量(万张) |
| TERM_YEAR | 借款期限(年) |
| COUPON_RATE | 利率(%) |
---
## 使用示例
### 示例1获取股票K线并判断是否涨停
```python
import asyncio
from app.adapters.amazingdata_adapter import AmazingDataAdapter
async def main():
adapter = AmazingDataAdapter()
await adapter.connect({
"username": "xxx", "password": "xxx",
"host": "xxx", "port": 8600
})
# 获取K线
klines = await adapter.fetch_klines("000001.SZ", "20240101", "20241231", "1d")
for k in klines:
print(f"日期: {k.trade_date}, 收盘: {k.close}, "
f"涨停: {k.is_limit_up}, 跌停: {k.is_limit_down}")
await adapter.close()
asyncio.run(main())
```
### 示例2获取财务报表
```python
# 获取资产负债表
balance = await adapter.get_balance_sheet(
codes=["000001.SZ", "600000.SH"],
start_date="20240930",
end_date="20240930"
)
for code, df in balance.items():
print(f"{code} 总资产: {df['TOTAL_ASSETS'].values[0]}")
```
### 示例3获取指数成分股
```python
# 沪深300成分股
constituents = await adapter.get_index_constituents(["000300.SH"])
df = constituents["000300.SH"]
print(f"成分股数量: {len(df)}")
print(df[["CON_CODE", "INDATE"]].head())
```
### 示例4获取龙虎榜数据
```python
longhu = await adapter.get_longhu_bang(
codes=["000001.SZ"],
start_date="20240101",
end_date="20241231"
)
print(longhu[["TRADE_DATE", "REASON_TYPE_NAME", "BUY_AMOUNT", "SELL_AMOUNT"]])
```
---
## 注意事项
1. **连接管理**: 使用前先调用 `connect()`,使用后调用 `close()`
2. **日期格式**: 支持 `YYYYMMDD`、`"YYYY-MM-DD"` 或 `date` 对象
3. **本地缓存**: 默认启用,可设置 `is_local=False` 强制从服务器获取
4. **批量处理**: 大量数据建议分批获取,每批 50-100 个代码
5. **错误处理**: 连接断开会抛出 `RuntimeError`,需做好异常处理
---
**文档版本**: 1.0
**更新日期**: 2024-03-11

@ -0,0 +1,922 @@
# AmazingData 数据源适配器 - 完整文档
## 目录
1. [适配器代码](#一适配器代码)
2. [接口详细说明](#二接口详细说明)
3. [使用示例](#三使用示例)
4. [数据结构说明](#四数据结构说明)
---
## 一、适配器代码
### 1. 主适配器代码 (amazing_data_adapter.py)
```python
"""
AmazingData 数据源适配器
基于银河证券星耀数智量化平台 SDK 的封装
提供统一、简洁的金融数据获取接口
"""
import pandas as pd
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime, date
from dataclasses import dataclass
from enum import Enum
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class SecurityType(Enum):
"""证券类型枚举"""
STOCK_A = "EXTRA_STOCK_A" # 沪深A股
STOCK_A_SH_SZ = "EXTRA_STOCK_A_SH_SZ" # 沪深A股沪深
INDEX_A = "EXTRA_INDEX_A" # 沪深指数
ETF = "EXTRA_ETF" # ETF
FUTURE = "EXTRA_FUTURE" # 期货
KZZ = "EXTRA_KZZ" # 可转债
GLRA = "EXTRA_GLRA" # 逆回购
HKT = "EXTRA_HKT" # 港股通
ETF_OP = "EXTRA_ETF_OP" # ETF期权
class Market(Enum):
"""市场枚举"""
SH = "SH" # 上海
SZ = "SZ" # 深圳
BJ = "BJ" # 北京
class Period(Enum):
"""周期枚举"""
MIN1 = "min1"
MIN5 = "min5"
MIN15 = "min15"
MIN30 = "min30"
MIN60 = "min60"
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
@dataclass
class DataSourceConfig:
"""数据源配置"""
username: str
password: str
host: str
port: int
local_path: str = "./amazing_data_cache/"
use_local_cache: bool = True
class AmazingDataAdapter:
"""
AmazingData 数据源适配器
封装银河证券星耀数智 SDK提供统一的数据获取接口
"""
def __init__(self, config: DataSourceConfig):
self.config = config
self._ad = None
self._base_data = None
self._market_data = None
self._info_data = None
self._calendar = None
self._is_logged_in = False
def connect(self) -> bool:
"""连接到数据源"""
try:
import AmazingData as ad
self._ad = ad
ad.login(
username=self.config.username,
password=self.config.password,
host=self.config.host,
port=self.config.port
)
self._base_data = ad.BaseData()
self._info_data = ad.InfoData()
self._calendar = self._base_data.get_calendar()
self._market_data = ad.MarketData(self._calendar)
self._is_logged_in = True
logger.info("成功连接到 AmazingData 数据源")
return True
except Exception as e:
logger.error(f"连接失败: {e}")
return False
def disconnect(self):
"""断开连接"""
if self._is_logged_in and self._ad:
try:
self._ad.logout(self.config.username)
logger.info("已断开与 AmazingData 的连接")
except Exception as e:
logger.warning(f"断开连接时出错: {e}")
self._is_logged_in = False
# ==================== 基础数据接口 ====================
def get_code_list(self, security_type: SecurityType = SecurityType.STOCK_A) -> List[str]:
"""
获取代码列表
Args:
security_type: 证券类型,可选 STOCK_A, ETF, FUTURE, KZZ, INDEX_A 等
Returns:
证券代码列表,如 ['000001.SZ', '600000.SH', ...]
"""
self._check_login()
if security_type == SecurityType.FUTURE:
return self._base_data.get_future_code_list(security_type=security_type.value)
elif security_type == SecurityType.ETF_OP:
return self._base_data.get_option_code_list(security_type=security_type.value)
else:
return self._base_data.get_code_list(security_type=security_type.value)
def get_code_info(self, security_type: SecurityType = SecurityType.STOCK_A) -> pd.DataFrame:
"""
获取证券信息
Returns:
DataFrame 包含字段:
- symbol: 证券简称
- security_status: 产品状态标志
- pre_close: 昨收价
- high_limited: 涨停价
- low_limited: 跌停价
- price_tick: 最小价格变动单位
"""
self._check_login()
return self._base_data.get_code_info(security_type=security_type.value)
def get_trading_calendar(self, market: Market = Market.SH) -> List[int]:
"""
获取交易日历
Returns:
交易日列表,格式为 [20240102, 20240103, ...]
"""
self._check_login()
return self._base_data.get_calendar(market=market.value)
def get_adj_factor(self, codes: List[str], is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取单次复权因子
Args:
codes: 股票代码列表
is_local: 是否使用本地缓存
Returns:
DataFrame (index: 日期, columns: 股票代码)
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._base_data.get_adj_factor(
code_list=codes, local_path=self.config.local_path, is_local=is_local
)
def get_backward_factor(self, codes: List[str], is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取后复权因子
Returns:
DataFrame (index: 日期, columns: 股票代码)
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._base_data.get_backward_factor(
code_list=codes, local_path=self.config.local_path, is_local=is_local
)
# ==================== 历史行情数据接口 ====================
def get_kline(self, codes: Union[str, List[str]],
start_date: Union[str, int, date],
end_date: Union[str, int, date],
period: Period = Period.DAILY) -> Dict[str, pd.DataFrame]:
"""
获取历史K线数据
Args:
codes: 证券代码或列表,如 '000001.SZ' 或 ['000001.SZ', '600000.SH']
start_date: 开始日期,支持 '20240101' 或 20240101 或 date对象
end_date: 结束日期
period: K线周期可选 MIN1/MIN5/MIN15/MIN30/MIN60/DAILY/WEEKLY/MONTHLY
Returns:
Dict[代码, DataFrame]DataFrame包含字段
- open: 开盘价
- high: 最高价
- low: 最低价
- close: 收盘价
- volume: 成交量
- amount: 成交金额
"""
self._check_login()
if isinstance(codes, str):
codes = [codes]
start_date = self._format_date(start_date)
end_date = self._format_date(end_date)
return self._market_data.query_kline(
code_list=codes, begin_date=start_date, end_date=end_date,
period=getattr(self._ad.constant.Period, period.value).value
)
def get_snapshot(self, codes: Union[str, List[str]],
start_date: Union[str, int, date],
end_date: Union[str, int, date]) -> Dict[str, pd.DataFrame]:
"""
获取历史快照数据
Returns:
Dict[代码, DataFrame]包含Level-1行情快照
"""
self._check_login()
if isinstance(codes, str):
codes = [codes]
start_date = self._format_date(start_date)
end_date = self._format_date(end_date)
return self._market_data.query_snapshot(
code_list=codes, begin_date=start_date, end_date=end_date
)
# ==================== 财务数据接口 ====================
def get_balance_sheet(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取资产负债表
Args:
codes: 股票代码列表
start_date: 开始报告期 (如 20240930 表示2024年三季报)
end_date: 结束报告期
Returns:
Dict[代码, DataFrame],主要字段包括:
- TOTAL_ASSETS: 资产总计
- TOTAL_CUR_ASSETS: 流动资产合计
- TOTAL_NONCUR_ASSETS: 非流动资产合计
- TOTAL_LIAB: 负债合计
- TOTAL_CUR_LIAB: 流动负债合计
- TOT_SHARE_EQUITY_INCL_MIN_INT: 股东权益合计
- CURRENCY_CAP: 货币资金
- NOTES_RECEIVABLE: 应收票据
- ACCT_RECEIVABLE: 应收账款
- INV: 存货
- FIX_ASSETS: 固定资产
- NOTES_PAYABLE: 应付票据
- ACCT_PAYABLE: 应付账款
- ST_BORROWING: 短期借款
- LT_LOAN: 长期借款
"""
return self._get_financial_data('get_balance_sheet', codes, start_date, end_date, is_local)
def get_cash_flow(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取现金流量表
主要字段:
- NET_CASH_FLOWS_OPERA_ACT: 经营活动现金流净额
- NET_CASH_FLOWS_INV_ACT: 投资活动现金流净额
- NET_CASH_FLOWS_FIN_ACT: 筹资活动现金流净额
- NET_INCR_CASH_AND_CASH_EQU: 现金及现金等价物净增加额
- CASH_RECP_SG_AND_RS: 销售商品提供劳务收到的现金
- CASH_PAY_GOODS_SERVICES: 购买商品接受劳务支付的现金
"""
return self._get_financial_data('get_cash_flow', codes, start_date, end_date, is_local)
def get_income_statement(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取利润表
主要字段:
- TOT_OPERA_REV: 营业总收入
- OPERA_REV: 营业收入
- TOT_OPERA_COST: 营业总成本
- OPERA_PROFIT: 营业利润
- TOTAL_PROFIT: 利润总额
- NET_PRO_INCL_MIN_INT_INC: 净利润
- BASIC_EPS: 基本每股收益
- DILUTED_EPS: 稀释每股收益
- RD_EXP: 研发费用
- LESS_SELLING_EXP: 销售费用
- LESS_ADMIN_EXP: 管理费用
- LESS_FIN_EXP: 财务费用
"""
return self._get_financial_data('get_income', codes, start_date, end_date, is_local)
def get_profit_express(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取业绩快报
主要字段:
- TOTAL_ASSETS: 总资产
- NET_PRO_EXCL_MIN_INT_INC: 净利润
- TOT_OPERA_REV: 营业总收入
- TOTAL_PROFIT: 利润总额
- OPERA_PROFIT: 营业利润
- EPS_BASIC: 基本每股收益
- ROE_WEIGHTED: 净资产收益率-加权
- YOY_GR_NET_PROFIT_PARENT: 同比增长率:归属母公司股东的净利润
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_profit_express(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_profit_notice(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取业绩预告
主要字段:
- P_TYPECODE: 业绩预告类型代码
- P_CHANGE_MAX: 预告净利润变动幅度上限
- P_CHANGE_MIN: 预告净利润变动幅度下限
- NET_PROFIT_MAX: 预告净利润上限(万元)
- NET_PROFIT_MIN: 预告净利润下限(万元)
- P_REASON: 业绩变动原因
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_profit_notice(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 股东股本数据接口 ====================
def get_top10_shareholders(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取十大股东数据
主要字段:
- HOLDER_NAME: 股东名称
- HOLDER_QUANTITY: 持股数
- HOLDER_PCT: 持股比例(%)
- HOLDER_HOLDER_CATEGORY: 股东性质(1:个人, 2:公司)
- FLOAT_QTY: 流通股数量
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_share_holder(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_shareholder_count(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取股东户数数据
主要字段:
- HOLDER_TOTAL_NUM: A股、B股、H股、境外股的总户数
- HOLDER_NUM: A股股东户数
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_holder_num(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_equity_structure(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取股本结构数据
主要字段:
- TOT_SHARE: 总股本(万股)
- FLOAT_SHARE: 流通股(万股)
- FLOAT_A_SHARE: 流通A股(万股)
- RESTRICTED_A_SHARE: 限售A股(万股)
- TOT_RESTRICTED_SHARE: 限售股合计
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_equity_structure(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 融资融券数据接口 ====================
def get_margin_summary(self,
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取融资融券成交汇总
主要字段:
- TRADE_DATE: 交易日期
- SUM_BORROW_MONEY_BALANCE: 融资余额(元)
- SUM_PURCH_WITH_BORROW_MONEY: 融资买入额(元)
- SUM_REPAYMENT_OF_BORROW_MONEY: 融资偿还额(元)
- SUM_SEC_LENDING_BALANCE: 融券余额(元)
- SUM_SALES_OF_BORROWED_SEC: 融券卖出量
- SUM_MARGIN_TRADE_BALANCE: 融资融券余额(元)
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_margin_summary(
local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_margin_detail(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取融资融券交易明细
主要字段:
- BORROW_MONEY_BALANCE: 融资余额
- PURCH_WITH_BORROW_MONEY: 融资买入额
- REPAYMENT_OF_BORROW_MONEY: 融资偿还额
- SEC_LENDING_BALANCE: 融券余额
- SALES_OF_BORROWED_SEC: 融券卖出量
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_margin_detail(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 交易异动数据接口 ====================
def get_longhu_bang(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取龙虎榜数据
主要字段:
- TRADE_DATE: 交易日期
- REASON_TYPE_NAME: 上榜原因
- CHANGE_RANGE: 涨跌幅(%)
- TRADER_NAME: 营业部名称
- BUY_AMOUNT: 买入金额(元)
- SELL_AMOUNT: 卖出金额(元)
- FLOW_MARK: 买卖表示(1买入, 2卖出)
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_long_hu_bang(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_block_trading(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取大宗交易数据
主要字段:
- TRADE_DATE: 交易日期
- B_SHARE_PRICE: 成交价(元)
- B_SHARE_VOLUME: 成交量(万股)
- B_SHARE_AMOUNT: 成交金额(万元)
- B_BUYER_NAME: 买方营业部名称
- B_SELLER_NAME: 卖方营业部名称
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_block_trading(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 指数数据接口 ====================
def get_index_constituents(self, codes: List[str],
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取指数成分股
Args:
codes: 指数代码列表,如 ['000300.SH', '000905.SH']
Returns:
Dict[指数代码, DataFrame]
DataFrame字段
- INDEX_CODE: 指数代码
- CON_CODE: 成分股代码
- INDATE: 纳入日期
- OUTDATE: 剔除日期
- INDEX_NAME: 指数名称
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_index_constituent(
code_list=codes, local_path=self.config.local_path, is_local=is_local
)
def get_index_weights(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取指数成分股权重
支持指数:
- 000016.SH: 上证50
- 000300.SH: 沪深300
- 000905.SH: 中证500
- 000906.SH: 中证800
- 000852.SH: 中证1000
DataFrame字段
- INDEX_CODE: 指数代码
- CON_CODE: 标的代码
- TRADE_DATE: 生效日期
- WEIGHT: 权重(%)
- CLOSE: 收盘价
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_index_weight(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== ETF数据接口 ====================
def get_etf_pcf(self, codes: List[str]) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
"""
获取ETF申赎数据
Returns:
(etf_info, etf_constituents)
etf_info字段
- creation_redemption_unit: 每个篮子对应的ETF份数
- max_cash_ratio: 最大现金替代比例
- creation: 是否允许申购
- redemption: 是否允许赎回
etf_constituents字段
- underlying_symbol: 成份证券简称
- component_share: 成份证券数量
- substitute_flag: 现金替代标志
"""
self._check_login()
return self._base_data.get_etf_pcf(code_list=codes)
def get_fund_share(self, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取基金份额数据
主要字段:
- FUND_SHARE: 基金份额(万份)
- TOTAL_SHARE: 基金总份额(万份)
- FLOAT_SHARE: 流通份额(万份)
- CHANGE_REASON: 份额变动原因
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_fund_share(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 可转债数据接口 ====================
def get_kzz_issuance(self, codes: List[str],
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取可转债发行数据
主要字段:
- STOCK_CODE: 正股代码
- LISTED_DATE: 上市日期
- PLAN_SCHEDULE: 方案进度
- CLAUSE_INI_CONV_PRICE: 初始转换价格
- LIST_ISSUE_SIZE: 发行规模(万元)
- LIST_ISSUE_QUANTITY: 发行数量(万张)
- TERM_YEAR: 借款期限(年)
- COUPON_RATE: 利率(%)
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_kzz_issuance(
code_list=codes, local_path=self.config.local_path, is_local=is_local
)
# ==================== 辅助方法 ====================
def _check_login(self):
if not self._is_logged_in:
raise RuntimeError("未连接到数据源,请先调用 connect()")
def _format_date(self, d: Union[str, int, date]) -> int:
if isinstance(d, int):
return d
elif isinstance(d, str):
return int(d.replace("-", "").replace("/", ""))
elif isinstance(d, date):
return int(d.strftime("%Y%m%d"))
else:
raise ValueError(f"不支持的日期格式: {d}")
def _get_financial_data(self, method: str, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
method = getattr(self._info_data, method)
return method(
code_list=codes, local_path=self.config.local_path, is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def create_adapter(username: str, password: str, host: str, port: int,
local_path: str = "./amazing_data_cache/",
use_local_cache: bool = True) -> AmazingDataAdapter:
"""快速创建适配器实例"""
config = DataSourceConfig(
username=username, password=password, host=host, port=port,
local_path=local_path, use_local_cache=use_local_cache
)
return AmazingDataAdapter(config)
```
---
## 二、接口详细说明
### 1. 基础数据接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_code_list` | 获取代码列表 | `List[str]` | 代码列表 |
| `get_code_info` | 获取证券信息 | `DataFrame` | symbol, pre_close, high_limited, low_limited |
| `get_trading_calendar` | 获取交易日历 | `List[int]` | 交易日列表 |
| `get_adj_factor` | 单次复权因子 | `DataFrame` | index=日期, columns=代码 |
| `get_backward_factor` | 后复权因子 | `DataFrame` | index=日期, columns=代码 |
### 2. 历史行情接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_kline` | K线数据 | `Dict[str, DataFrame]` | open, high, low, close, volume, amount |
| `get_snapshot` | 历史快照 | `Dict[str, DataFrame]` | Level-1行情数据 |
**Period 周期选项:**
- `Period.MIN1` / `Period.MIN5` / `Period.MIN15` / `Period.MIN30` / `Period.MIN60`
- `Period.DAILY` / `Period.WEEKLY` / `Period.MONTHLY`
### 3. 财务数据接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_balance_sheet` | 资产负债表 | `Dict[str, DataFrame]` | TOTAL_ASSETS, TOTAL_LIAB, TOT_SHARE_EQUITY |
| `get_cash_flow` | 现金流量表 | `Dict[str, DataFrame]` | NET_CASH_FLOWS_OPERA_ACT, NET_CASH_FLOWS_INV_ACT |
| `get_income_statement` | 利润表 | `Dict[str, DataFrame]` | TOT_OPERA_REV, NET_PRO_INCL_MIN_INT_INC, BASIC_EPS |
| `get_profit_express` | 业绩快报 | `DataFrame` | ROE_WEIGHTED, YOY_GR_NET_PROFIT_PARENT |
| `get_profit_notice` | 业绩预告 | `DataFrame` | P_CHANGE_MAX, P_CHANGE_MIN, NET_PROFIT_MAX |
### 4. 股东股本接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_top10_shareholders` | 十大股东 | `DataFrame` | HOLDER_NAME, HOLDER_QUANTITY, HOLDER_PCT |
| `get_shareholder_count` | 股东户数 | `DataFrame` | HOLDER_TOTAL_NUM, HOLDER_NUM |
| `get_equity_structure` | 股本结构 | `DataFrame` | TOT_SHARE, FLOAT_SHARE, RESTRICTED_A_SHARE |
### 5. 融资融券接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_margin_summary` | 融资融券汇总 | `DataFrame` | SUM_BORROW_MONEY_BALANCE, SUM_MARGIN_TRADE_BALANCE |
| `get_margin_detail` | 个股融资融券 | `Dict[str, DataFrame]` | BORROW_MONEY_BALANCE, SEC_LENDING_BALANCE |
### 6. 交易异动接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_longhu_bang` | 龙虎榜 | `DataFrame` | TRADER_NAME, BUY_AMOUNT, SELL_AMOUNT, FLOW_MARK |
| `get_block_trading` | 大宗交易 | `DataFrame` | B_SHARE_PRICE, B_SHARE_VOLUME, B_BUYER_NAME |
### 7. 指数数据接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_index_constituents` | 指数成分股 | `Dict[str, DataFrame]` | INDEX_CODE, CON_CODE, INDATE, OUTDATE |
| `get_index_weights` | 成分股权重 | `Dict[str, DataFrame]` | INDEX_CODE, CON_CODE, WEIGHT |
**支持的指数代码:**
- `000016.SH` - 上证50
- `000300.SH` - 沪深300
- `000905.SH` - 中证500
- `000906.SH` - 中证800
- `000852.SH` - 中证1000
### 8. ETF数据接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_etf_pcf` | ETF申赎数据 | `Tuple[DataFrame, Dict]` | creation_redemption_unit, component_share |
| `get_fund_share` | 基金份额 | `Dict[str, DataFrame]` | FUND_SHARE, TOTAL_SHARE |
### 9. 可转债数据接口
| 接口名 | 功能 | 返回类型 | 主要字段 |
|--------|------|----------|----------|
| `get_kzz_issuance` | 可转债发行 | `Dict[str, DataFrame]` | CLAUSE_INI_CONV_PRICE, LIST_ISSUE_SIZE, COUPON_RATE |
---
## 三、使用示例
### 基础使用
```python
from amazing_data_adapter import create_adapter, SecurityType, Period
# 创建并连接
adapter = create_adapter(
username='your_username',
password='your_password',
host='your_host',
port=8080
)
if adapter.connect():
# 获取A股列表
codes = adapter.get_code_list(SecurityType.STOCK_A)
print(f"A股数量: {len(codes)}")
# 获取平安银行K线
kline = adapter.get_kline(
codes=['000001.SZ'],
start_date='20240101',
end_date='20241231',
period=Period.DAILY
)
print(kline['000001.SZ'].head())
adapter.disconnect()
```
### 获取财务数据
```python
# 获取资产负债表
balance = adapter.get_balance_sheet(
codes=['000001.SZ', '600000.SH'],
start_date=20240930,
end_date=20240930
)
for code, df in balance.items():
print(f"{code} 总资产: {df['TOTAL_ASSETS'].values[0]}")
```
### 获取指数成分股
```python
# 沪深300成分股
constituents = adapter.get_index_constituents(['000300.SH'])
df = constituents['000300.SH']
print(f"成分股数量: {len(df)}")
print(df[['CON_CODE', 'INDATE']].head())
```
### 批量处理
```python
# 分批获取数据避免超时
all_codes = adapter.get_code_list(SecurityType.STOCK_A)
batch_size = 50
for i in range(0, 100, batch_size): # 只取前100只演示
batch = all_codes[i:i+batch_size]
data = adapter.get_balance_sheet(batch)
print(f"已处理 {i+len(batch)} 只股票")
```
---
## 四、数据结构说明
### K线数据字段
| 字段名 | 类型 | 说明 |
|--------|------|------|
| open | float | 开盘价 |
| high | float | 最高价 |
| low | float | 最低价 |
| close | float | 收盘价 |
| volume | int | 成交量(股) |
| amount | float | 成交金额(元) |
### 资产负债表关键字段
| 字段名 | 说明 |
|--------|------|
| TOTAL_ASSETS | 资产总计 |
| TOTAL_CUR_ASSETS | 流动资产合计 |
| TOTAL_NONCUR_ASSETS | 非流动资产合计 |
| TOTAL_LIAB | 负债合计 |
| TOT_SHARE_EQUITY_INCL_MIN_INT | 股东权益合计(含少数股东) |
| CURRENCY_CAP | 货币资金 |
| NOTES_RECEIVABLE | 应收票据 |
| ACCT_RECEIVABLE | 应收账款 |
| INV | 存货 |
### 利润表关键字段
| 字段名 | 说明 |
|--------|------|
| TOT_OPERA_REV | 营业总收入 |
| OPERA_PROFIT | 营业利润 |
| TOTAL_PROFIT | 利润总额 |
| NET_PRO_INCL_MIN_INT_INC | 净利润(含少数股东) |
| BASIC_EPS | 基本每股收益 |
| DILUTED_EPS | 稀释每股收益 |
### 现金流量表关键字段
| 字段名 | 说明 |
|--------|------|
| NET_CASH_FLOWS_OPERA_ACT | 经营活动现金流净额 |
| NET_CASH_FLOWS_INV_ACT | 投资活动现金流净额 |
| NET_CASH_FLOWS_FIN_ACT | 筹资活动现金流净额 |
| NET_INCR_CASH_AND_CASH_EQU | 现金及现金等价物净增加额 |
---
## 五、注意事项
1. **连接管理**: 使用前先调用 `connect()`,使用后调用 `disconnect()`
2. **日期格式**: 支持 `20240101`、`"2024-01-01"` 或 `date` 对象
3. **本地缓存**: 默认启用,可设置 `is_local=False` 强制从服务器获取
4. **批量处理**: 大量数据建议分批获取,每批 50-100 个代码
5. **错误处理**: 连接断开会抛出 `RuntimeError`,需做好异常处理
---
**文件保存位置**: `/root/.openclaw/workspace/amazing_data_adapter.py`

File diff suppressed because it is too large Load Diff

@ -0,0 +1,268 @@
# AmazingData 数据源适配器
基于中国银河证券星耀数智量化平台 SDK 的封装,提供统一、简洁的金融数据获取接口。
## 功能特性
- **简洁的API设计**: 封装复杂的SDK接口提供直观的数据获取方法
- **类型安全**: 使用Python类型注解IDE友好的代码提示
- **灵活的配置**: 支持本地缓存、参数自定义等配置选项
- **全面的数据覆盖**: 支持行情、财务、股本、融资融券等多类金融数据
## 安装依赖
```bash
# 安装 AmazingData SDK (需从银河证券获取)
pip install tgw-1.*.*-py3-none-any.whl
pip install AmazingData-1.*.*-cp3x-none-any.whl
# 安装其他依赖
pip install pandas
```
## 快速开始
```python
from amazing_data_adapter import create_adapter, SecurityType, Period
# 1. 创建适配器
adapter = create_adapter(
username='your_username',
password='your_password',
host='your_host',
port=8080,
local_path='./data_cache/', # 本地缓存路径
use_local_cache=True # 是否使用本地缓存
)
# 2. 连接数据源
if adapter.connect():
# 3. 获取数据
codes = adapter.get_code_list(SecurityType.STOCK_A)
kline = adapter.get_kline(
codes=['000001.SZ'],
start_date='20240101',
end_date='20241231',
period=Period.DAILY
)
# 4. 断开连接
adapter.disconnect()
```
## 功能模块
### 1. 基础数据
| 方法 | 说明 |
|------|------|
| `get_code_list(security_type)` | 获取代码列表 |
| `get_code_info(security_type)` | 获取证券基本信息 |
| `get_trading_calendar(market)` | 获取交易日历 |
| `get_adj_factor(codes)` | 获取单次复权因子 |
| `get_backward_factor(codes)` | 获取后复权因子 |
### 2. 历史行情数据
| 方法 | 说明 |
|------|------|
| `get_kline(codes, start_date, end_date, period)` | 获取K线数据 |
| `get_snapshot(codes, start_date, end_date)` | 获取历史快照 |
**支持的周期 (Period)**:
- `Period.MIN1` - 1分钟
- `Period.MIN5` - 5分钟
- `Period.MIN15` - 15分钟
- `Period.MIN30` - 30分钟
- `Period.MIN60` - 60分钟
- `Period.DAILY` - 日线
- `Period.WEEKLY` - 周线
- `Period.MONTHLY` - 月线
### 3. 财务数据
| 方法 | 说明 |
|------|------|
| `get_balance_sheet(codes, start_date, end_date)` | 资产负债表 |
| `get_cash_flow(codes, start_date, end_date)` | 现金流量表 |
| `get_income_statement(codes, start_date, end_date)` | 利润表 |
| `get_profit_express(codes, start_date, end_date)` | 业绩快报 |
| `get_profit_notice(codes, start_date, end_date)` | 业绩预告 |
### 4. 股东股本数据
| 方法 | 说明 |
|------|------|
| `get_top10_shareholders(codes, start_date, end_date)` | 十大股东 |
| `get_shareholder_count(codes, start_date, end_date)` | 股东户数 |
| `get_equity_structure(codes, start_date, end_date)` | 股本结构 |
### 5. 融资融券数据
| 方法 | 说明 |
|------|------|
| `get_margin_summary(start_date, end_date)` | 融资融券汇总 |
| `get_margin_detail(codes, start_date, end_date)` | 个股融资融券明细 |
### 6. 交易异动数据
| 方法 | 说明 |
|------|------|
| `get_longhu_bang(codes, start_date, end_date)` | 龙虎榜数据 |
| `get_block_trading(codes, start_date, end_date)` | 大宗交易 |
### 7. 指数数据
| 方法 | 说明 |
|------|------|
| `get_index_constituents(codes)` | 指数成分股 |
| `get_index_weights(codes, start_date, end_date)` | 成分股权重 |
**支持的指数**:
- `000016.SH` - 上证50
- `000300.SH` - 沪深300
- `000905.SH` - 中证500
- `000906.SH` - 中证800
- `000852.SH` - 中证1000
### 8. ETF数据
| 方法 | 说明 |
|------|------|
| `get_etf_pcf(codes)` | ETF申赎数据 |
| `get_fund_share(codes, start_date, end_date)` | 基金份额 |
### 9. 可转债数据
| 方法 | 说明 |
|------|------|
| `get_kzz_issuance(codes)` | 可转债发行数据 |
## 证券类型枚举
```python
from amazing_data_adapter import SecurityType
SecurityType.STOCK_A # 沪深A股
SecurityType.STOCK_A_SH_SZ # 沪深A股沪深
SecurityType.INDEX_A # 沪深指数
SecurityType.ETF # ETF
SecurityType.FUTURE # 期货
SecurityType.KZZ # 可转债
SecurityType.GLRA # 逆回购
SecurityType.HKT # 港股通
SecurityType.ETF_OP # ETF期权
```
## 使用示例
### 获取历史K线数据
```python
# 获取多只股票日线数据
kline_data = adapter.get_kline(
codes=['000001.SZ', '600000.SH'],
start_date='20240101',
end_date='20241231',
period=Period.DAILY
)
for code, df in kline_data.items():
print(f"{code}: {len(df)} 条数据")
print(df.head())
```
### 获取财务报表
```python
# 获取资产负债表
balance_sheet = adapter.get_balance_sheet(
codes=['000001.SZ', '600000.SH'],
start_date=20240101,
end_date=20241231
)
for code, df in balance_sheet.items():
print(f"\n{code} 资产负债表:")
print(df[['REPORTING_PERIOD', 'TOTAL_ASSETS', 'TOTAL_CUR_ASSETS']])
```
### 获取指数成分股
```python
# 获取沪深300成分股
constituents = adapter.get_index_constituents(['000300.SH'])
df = constituents['000300.SH']
print(f"沪深300成分股数量: {len(df)}")
print(df[['CON_CODE', 'INDATE', 'INDEX_NAME']].head())
```
### 批量数据处理
```python
# 获取所有A股代码
all_codes = adapter.get_code_list(SecurityType.STOCK_A)
# 分批处理避免超时
batch_size = 50
for i in range(0, len(all_codes), batch_size):
batch_codes = all_codes[i:i+batch_size]
data = adapter.get_balance_sheet(batch_codes)
# 处理数据...
```
### 结合复权因子计算真实价格
```python
# 获取K线和复权因子
kline = adapter.get_kline(['000001.SZ'], '20240101', '20241231')
adj_factor = adapter.get_backward_factor(['000001.SZ'])
df = kline['000001.SZ']
# 合并并计算复权价格
df['trade_date'] = df.index.strftime('%Y%m%d').astype(int)
df = df.merge(adj_factor[['000001.SZ']].reset_index(),
left_on='trade_date', right_on='index')
df['adj_close'] = df['close'] * df['000001.SZ']
```
## 数据缓存
适配器支持本地数据缓存,可大幅提升重复查询的速度:
```python
adapter = create_adapter(
username='xxx',
password='xxx',
host='xxx',
port=8080,
local_path='./my_data_cache/', # 缓存目录
use_local_cache=True # 默认启用缓存
)
# 强制从服务器获取最新数据
adapter.get_kline(codes, start_date, end_date, is_local=False)
```
## 注意事项
1. **账号权限**: 使用本适配器需要先向中国银河证券申请开通星耀数智平台权限
2. **日期格式**: 支持多种日期格式:
- `int`: 20240101
- `str`: "2024-01-01" 或 "20240101"
- `date`: datetime.date(2024, 1, 1)
3. **错误处理**: 所有方法在连接断开会抛出 `RuntimeError`,建议在外层做好异常处理
4. **资源释放**: 使用完毕后请调用 `adapter.disconnect()` 断开连接
## 文件说明
- `amazing_data_adapter.py` - 适配器主代码
- `amazing_data_examples.py` - 详细使用示例
- `README.md` - 本文档
## API参考
详细的SDK接口文档请参考银河证券提供的《AmazingData开发手册》。

@ -0,0 +1,832 @@
"""
AmazingData 数据源适配器
基于银河证券星耀数智量化平台 SDK 的封装
提供统一简洁的金融数据获取接口
"""
import pandas as pd
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime, date
from dataclasses import dataclass
from enum import Enum
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class SecurityType(Enum):
"""证券类型枚举"""
STOCK_A = "EXTRA_STOCK_A" # 沪深A股
STOCK_A_SH_SZ = "EXTRA_STOCK_A_SH_SZ" # 沪深A股沪深
INDEX_A = "EXTRA_INDEX_A" # 沪深指数
ETF = "EXTRA_ETF" # ETF
FUTURE = "EXTRA_FUTURE" # 期货
KZZ = "EXTRA_KZZ" # 可转债
GLRA = "EXTRA_GLRA" # 逆回购
HKT = "EXTRA_HKT" # 港股通
ETF_OP = "EXTRA_ETF_OP" # ETF期权
class Market(Enum):
"""市场枚举"""
SH = "SH" # 上海
SZ = "SZ" # 深圳
BJ = "BJ" # 北京
class Period(Enum):
"""周期枚举"""
MIN1 = "min1"
MIN5 = "min5"
MIN15 = "min15"
MIN30 = "min30"
MIN60 = "min60"
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
@dataclass
class DataSourceConfig:
"""数据源配置"""
username: str
password: str
host: str
port: int
local_path: str = "./amazing_data_cache/"
use_local_cache: bool = True
class AmazingDataAdapter:
"""
AmazingData 数据源适配器
封装银河证券星耀数智 SDK提供统一的数据获取接口
"""
def __init__(self, config: DataSourceConfig):
"""
初始化适配器
Args:
config: 数据源配置
"""
self.config = config
self._ad = None
self._base_data = None
self._market_data = None
self._info_data = None
self._calendar = None
self._is_logged_in = False
def connect(self) -> bool:
"""
连接到数据源
Returns:
bool: 是否连接成功
"""
try:
import AmazingData as ad
self._ad = ad
# 登录
ad.login(
username=self.config.username,
password=self.config.password,
host=self.config.host,
port=self.config.port
)
# 初始化数据类
self._base_data = ad.BaseData()
self._info_data = ad.InfoData()
self._calendar = self._base_data.get_calendar()
self._market_data = ad.MarketData(self._calendar)
self._is_logged_in = True
logger.info("成功连接到 AmazingData 数据源")
return True
except Exception as e:
logger.error(f"连接失败: {e}")
return False
def disconnect(self):
"""断开连接"""
if self._is_logged_in and self._ad:
try:
self._ad.logout(self.config.username)
logger.info("已断开与 AmazingData 的连接")
except Exception as e:
logger.warning(f"断开连接时出错: {e}")
self._is_logged_in = False
# ==================== 基础数据接口 ====================
def get_code_list(self, security_type: SecurityType = SecurityType.STOCK_A) -> List[str]:
"""
获取代码列表
Args:
security_type: 证券类型
Returns:
证券代码列表
"""
self._check_login()
if security_type == SecurityType.FUTURE:
return self._base_data.get_future_code_list(security_type=security_type.value)
elif security_type == SecurityType.ETF_OP:
return self._base_data.get_option_code_list(security_type=security_type.value)
else:
return self._base_data.get_code_list(security_type=security_type.value)
def get_code_info(self, security_type: SecurityType = SecurityType.STOCK_A) -> pd.DataFrame:
"""
获取证券信息
Args:
security_type: 证券类型
Returns:
DataFrame 包含证券基本信息
"""
self._check_login()
return self._base_data.get_code_info(security_type=security_type.value)
def get_trading_calendar(self, market: Market = Market.SH) -> List[int]:
"""
获取交易日历
Args:
market: 市场
Returns:
交易日列表 (YYYYMMDD 格式)
"""
self._check_login()
return self._base_data.get_calendar(market=market.value)
def get_adj_factor(self, codes: List[str],
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取复权因子单次复权
Args:
codes: 股票代码列表
is_local: 是否使用本地缓存
Returns:
DataFrame (index: 日期, columns: 股票代码)
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._base_data.get_adj_factor(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local
)
def get_backward_factor(self, codes: List[str],
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取后复权因子
Args:
codes: 股票代码列表
is_local: 是否使用本地缓存
Returns:
DataFrame (index: 日期, columns: 股票代码)
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._base_data.get_backward_factor(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local
)
# ==================== 历史行情数据接口 ====================
def get_kline(self,
codes: Union[str, List[str]],
start_date: Union[str, int, date],
end_date: Union[str, int, date],
period: Period = Period.DAILY) -> Dict[str, pd.DataFrame]:
"""
获取历史K线数据
Args:
codes: 证券代码或代码列表
start_date: 开始日期
end_date: 结束日期
period: K线周期
Returns:
Dict[代码, DataFrame]DataFrame包含OHLCV等字段
"""
self._check_login()
if isinstance(codes, str):
codes = [codes]
start_date = self._format_date(start_date)
end_date = self._format_date(end_date)
# 获取K线数据
kline_dict = self._market_data.query_kline(
code_list=codes,
begin_date=start_date,
end_date=end_date,
period=getattr(self._ad.constant.Period, period.value).value
)
return kline_dict
def get_snapshot(self,
codes: Union[str, List[str]],
start_date: Union[str, int, date],
end_date: Union[str, int, date]) -> Dict[str, pd.DataFrame]:
"""
获取历史快照数据tick级别
Args:
codes: 证券代码或代码列表
start_date: 开始日期
end_date: 结束日期
Returns:
Dict[代码, DataFrame]
"""
self._check_login()
if isinstance(codes, str):
codes = [codes]
start_date = self._format_date(start_date)
end_date = self._format_date(end_date)
snapshot_dict = self._market_data.query_snapshot(
code_list=codes,
begin_date=start_date,
end_date=end_date
)
return snapshot_dict
# ==================== 财务数据接口 ====================
def get_balance_sheet(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取资产负债表
Args:
codes: 股票代码列表
start_date: 开始报告期
end_date: 结束报告期
is_local: 是否使用本地缓存
Returns:
Dict[代码, DataFrame]
"""
return self._get_financial_data(
'get_balance_sheet', codes, start_date, end_date, is_local
)
def get_cash_flow(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取现金流量表
Args:
codes: 股票代码列表
start_date: 开始报告期
end_date: 结束报告期
is_local: 是否使用本地缓存
Returns:
Dict[代码, DataFrame]
"""
return self._get_financial_data(
'get_cash_flow', codes, start_date, end_date, is_local
)
def get_income_statement(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取利润表
Args:
codes: 股票代码列表
start_date: 开始报告期
end_date: 结束报告期
is_local: 是否使用本地缓存
Returns:
Dict[代码, DataFrame]
"""
return self._get_financial_data(
'get_income', codes, start_date, end_date, is_local
)
def get_profit_express(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取业绩快报
Args:
codes: 股票代码列表
start_date: 开始报告期
end_date: 结束报告期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_profit_express(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_profit_notice(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取业绩预告
Args:
codes: 股票代码列表
start_date: 开始报告期
end_date: 结束报告期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_profit_notice(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 股东股本数据接口 ====================
def get_top10_shareholders(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取十大股东数据
Args:
codes: 股票代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_share_holder(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_shareholder_count(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取股东户数数据
Args:
codes: 股票代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_holder_num(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_equity_structure(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取股本结构数据
Args:
codes: 股票代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_equity_structure(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 融资融券数据接口 ====================
def get_margin_summary(self,
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取融资融券成交汇总
Args:
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_margin_summary(
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_margin_detail(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取融资融券交易明细
Args:
codes: 股票代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
Dict[代码, DataFrame]
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_margin_detail(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 交易异动数据接口 ====================
def get_longhu_bang(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取龙虎榜数据
Args:
codes: 股票代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_long_hu_bang(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
def get_block_trading(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> pd.DataFrame:
"""
获取大宗交易数据
Args:
codes: 股票代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
DataFrame
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_block_trading(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 指数数据接口 ====================
def get_index_constituents(self,
codes: List[str],
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取指数成分股
Args:
codes: 指数代码列表
is_local: 是否使用本地缓存
Returns:
Dict[指数代码, DataFrame]
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_index_constituent(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local
)
def get_index_weights(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取指数成分股权重
支持上证50(000016.SH)沪深300(000300.SH)中证500(000905.SH)
中证800(000906.SH)中证1000(000852.SH)
Args:
codes: 指数代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
Dict[指数代码, DataFrame]
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_index_weight(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== ETF数据接口 ====================
def get_etf_pcf(self, codes: List[str]) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
"""
获取ETF申赎数据
Args:
codes: ETF代码列表
Returns:
(etf_info, etf_constituents)
"""
self._check_login()
return self._base_data.get_etf_pcf(code_list=codes)
def get_fund_share(self,
codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取基金份额数据
Args:
codes: ETF代码列表
start_date: 开始日期
end_date: 结束日期
is_local: 是否使用本地缓存
Returns:
Dict[代码, DataFrame]
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_fund_share(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 可转债数据接口 ====================
def get_kzz_issuance(self,
codes: List[str],
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""
获取可转债发行数据
Args:
codes: 可转债代码列表
is_local: 是否使用本地缓存
Returns:
Dict[代码, DataFrame]
"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
return self._info_data.get_kzz_issuance(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local
)
# ==================== 辅助方法 ====================
def _check_login(self):
"""检查是否已登录"""
if not self._is_logged_in:
raise RuntimeError("未连接到数据源,请先调用 connect()")
def _format_date(self, d: Union[str, int, date]) -> int:
"""统一日期格式为 YYYYMMDD"""
if isinstance(d, int):
return d
elif isinstance(d, str):
return int(d.replace("-", "").replace("/", ""))
elif isinstance(d, date):
return int(d.strftime("%Y%m%d"))
else:
raise ValueError(f"不支持的日期格式: {d}")
def _get_financial_data(self, method: str, codes: List[str],
start_date: Optional[Union[str, int, date]] = None,
end_date: Optional[Union[str, int, date]] = None,
is_local: Optional[bool] = None) -> Dict[str, pd.DataFrame]:
"""通用财务数据获取方法"""
self._check_login()
is_local = is_local if is_local is not None else self.config.use_local_cache
method = getattr(self._info_data, method)
return method(
code_list=codes,
local_path=self.config.local_path,
is_local=is_local,
begin_date=self._format_date(start_date) if start_date else None,
end_date=self._format_date(end_date) if end_date else None
)
# ==================== 便捷函数 ====================
def create_adapter(username: str, password: str, host: str, port: int,
local_path: str = "./amazing_data_cache/",
use_local_cache: bool = True) -> AmazingDataAdapter:
"""
快速创建适配器实例
Args:
username: 用户名
password: 密码
host: 服务器地址
port: 服务器端口
local_path: 本地缓存路径
use_local_cache: 是否使用本地缓存
Returns:
AmazingDataAdapter 实例
"""
config = DataSourceConfig(
username=username,
password=password,
host=host,
port=port,
local_path=local_path,
use_local_cache=use_local_cache
)
return AmazingDataAdapter(config)
# ==================== 使用示例 ====================
if __name__ == "__main__":
# 示例代码
print("""
# 使用示例:
# 1. 创建适配器
adapter = create_adapter(
username='your_username',
password='your_password',
host='your_host',
port=your_port
)
# 2. 连接数据源
if adapter.connect():
# 3. 获取沪深A股代码列表
codes = adapter.get_code_list(SecurityType.STOCK_A)
print(f"获取到 {len(codes)} 只股票")
# 4. 获取历史K线数据
kline_data = adapter.get_kline(
codes=['000001.SZ', '600000.SH'],
start_date='20240101',
end_date='20241231',
period=Period.DAILY
)
# 5. 获取财务数据
balance_sheet = adapter.get_balance_sheet(
codes=['000001.SZ', '600000.SH'],
start_date='20240101',
end_date='20241231'
)
# 6. 获取指数成分股
constituents = adapter.get_index_constituents(['000300.SH']) # 沪深300
# 7. 断开连接
adapter.disconnect()
""")

@ -0,0 +1,379 @@
"""
AmazingData 数据源适配器 - 使用示例
本文件展示了如何使用 AmazingDataAdapter 获取各类金融数据
"""
from amazing_data_adapter import (
AmazingDataAdapter,
DataSourceConfig,
SecurityType,
Market,
Period,
create_adapter
)
import pandas as pd
def demo_basic_usage():
"""基础使用示例"""
# 方式1: 使用配置对象创建
config = DataSourceConfig(
username='your_username',
password='your_password',
host='your_host',
port=8080,
local_path='./amazing_data_cache/',
use_local_cache=True
)
adapter = AmazingDataAdapter(config)
# 方式2: 使用便捷函数创建
# adapter = create_adapter(
# username='your_username',
# password='your_password',
# host='your_host',
# port=8080
# )
# 连接数据源
if not adapter.connect():
print("连接失败")
return
try:
# ============== 基础数据 ==============
print("\n=== 基础数据 ===")
# 获取沪深A股代码列表
stock_codes = adapter.get_code_list(SecurityType.STOCK_A)
print(f"沪深A股数量: {len(stock_codes)}")
# 获取ETF代码列表
etf_codes = adapter.get_code_list(SecurityType.ETF)
print(f"ETF数量: {len(etf_codes)}")
# 获取期货代码列表
future_codes = adapter.get_code_list(SecurityType.FUTURE)
print(f"期货数量: {len(future_codes)}")
# 获取证券基本信息
stock_info = adapter.get_code_info(SecurityType.STOCK_A)
print(f"\n证券信息列: {stock_info.columns.tolist()}")
print(stock_info.head())
# 获取交易日历
calendar = adapter.get_trading_calendar(Market.SH)
print(f"\n交易日数量: {len(calendar)}")
print(f"最近交易日: {calendar[-5:]}")
# ============== 历史行情数据 ==============
print("\n=== 历史行情数据 ===")
# 获取日K线数据
sample_codes = stock_codes[:5] # 取前5只股票作为示例
kline_data = adapter.get_kline(
codes=sample_codes,
start_date='20240101',
end_date='20241231',
period=Period.DAILY
)
for code, df in list(kline_data.items())[:2]:
print(f"\n{code} K线数据:")
print(df.head())
# 获取分钟K线
min_kline = adapter.get_kline(
codes=['000001.SZ'],
start_date='20241201',
end_date='20241231',
period=Period.MIN60
)
# ============== 复权因子 ==============
print("\n=== 复权因子 ===")
adj_factor = adapter.get_adj_factor(
codes=['000001.SZ', '600000.SH'],
is_local=False # 强制从服务器获取最新数据
)
print("单次复权因子:")
print(adj_factor.head())
backward_factor = adapter.get_backward_factor(
codes=['000001.SZ', '600000.SH']
)
print("\n后复权因子:")
print(backward_factor.head())
# ============== 财务数据 ==============
print("\n=== 财务数据 ===")
# 获取资产负债表
balance_sheet = adapter.get_balance_sheet(
codes=['000001.SZ', '600000.SH'],
start_date=20240101,
end_date=20241231
)
for code, df in balance_sheet.items():
print(f"\n{code} 资产负债表字段:")
print(df.columns.tolist()[:10]) # 显示前10个字段
# 获取现金流量表
cash_flow = adapter.get_cash_flow(
codes=['000001.SZ'],
start_date=20240101,
end_date=20241231
)
# 获取利润表
income = adapter.get_income_statement(
codes=['000001.SZ'],
start_date=20240101,
end_date=20241231
)
# 获取业绩快报
profit_express = adapter.get_profit_express(
codes=['000001.SZ', '600000.SH']
)
print(f"\n业绩快报:\n{profit_express.head()}")
# ============== 股东股本数据 ==============
print("\n=== 股东股本数据 ===")
# 获取十大股东
top10_holders = adapter.get_top10_shareholders(
codes=['000001.SZ'],
start_date=20240101,
end_date=20241231
)
print(f"十大股东:\n{top10_holders.head()}")
# 获取股东户数
holder_count = adapter.get_shareholder_count(
codes=['000001.SZ'],
start_date=20240101,
end_date=20241231
)
print(f"\n股东户数:\n{holder_count.head()}")
# 获取股本结构
equity_structure = adapter.get_equity_structure(
codes=['000001.SZ']
)
print(f"\n股本结构:\n{equity_structure.head()}")
# ============== 融资融券数据 ==============
print("\n=== 融资融券数据 ===")
# 获取融资融券汇总
margin_summary = adapter.get_margin_summary(
start_date=20240101,
end_date=20241231
)
print(f"融资融券汇总:\n{margin_summary.head()}")
# 获取个股融资融券明细
margin_detail = adapter.get_margin_detail(
codes=['000001.SZ'],
start_date=20241201,
end_date=20241231
)
# ============== 交易异动数据 ==============
print("\n=== 交易异动数据 ===")
# 获取龙虎榜
longhu = adapter.get_longhu_bang(
codes=['000001.SZ'],
start_date=20241201,
end_date=20241231
)
print(f"龙虎榜数据:\n{longhu.head()}")
# 获取大宗交易
block_trade = adapter.get_block_trading(
codes=['000001.SZ'],
start_date=20241201,
end_date=20241231
)
print(f"\n大宗交易:\n{block_trade.head()}")
# ============== 指数数据 ==============
print("\n=== 指数数据 ===")
# 获取指数成分股 (沪深300)
index_constituents = adapter.get_index_constituents(['000300.SH'])
for code, df in index_constituents.items():
print(f"\n{code} 成分股数量: {len(df)}")
print(df.head())
# 获取指数成分股权重
index_weights = adapter.get_index_weights(
codes=['000300.SH', '000905.SH'], # 沪深300和中证500
start_date=20241201,
end_date=20241231
)
# ============== ETF数据 ==============
print("\n=== ETF数据 ===")
# 获取ETF申赎数据
etf_info, etf_constituents = adapter.get_etf_pcf(['510050.SH'])
print(f"ETF基本信息:\n{etf_info.head()}")
# 获取基金份额
fund_share = adapter.get_fund_share(
codes=['510050.SH'],
start_date=20240101,
end_date=20241231
)
# ============== 可转债数据 ==============
print("\n=== 可转债数据 ===")
# 获取可转债发行数据
kzz_codes = adapter.get_code_list(SecurityType.KZZ)
print(f"可转债数量: {len(kzz_codes)}")
if kzz_codes:
kzz_issuance = adapter.get_kzz_issuance(kzz_codes[:3])
for code, df in list(kzz_issuance.items())[:1]:
print(f"\n{code} 发行信息:\n{df.head()}")
finally:
# 断开连接
adapter.disconnect()
def demo_data_processing():
"""数据处理示例 - 展示如何加工获取的数据"""
adapter = create_adapter(
username='your_username',
password='your_password',
host='your_host',
port=8080
)
if not adapter.connect():
return
try:
# 获取平安银行(000001.SZ)的日K线数据
kline_data = adapter.get_kline(
codes=['000001.SZ'],
start_date='20240101',
end_date='20241231',
period=Period.DAILY
)
df = kline_data['000001.SZ']
# 计算技术指标
# 1. 移动平均线
df['MA5'] = df['close'].rolling(window=5).mean()
df['MA10'] = df['close'].rolling(window=10).mean()
df['MA20'] = df['close'].rolling(window=20).mean()
# 2. 涨跌幅
df['return'] = df['close'].pct_change()
# 3. 波动率 (20日)
df['volatility'] = df['return'].rolling(window=20).std() * (252 ** 0.5)
# 4. 成交量移动平均
df['volume_MA5'] = df['volume'].rolling(window=5).mean()
print("加工后的数据:")
print(df[['close', 'MA5', 'MA10', 'MA20', 'return', 'volatility']].tail(10))
# 获取复权因子并计算复权价格
adj_factor = adapter.get_backward_factor(['000001.SZ'])
# 合并K线和复权因子
df['trade_date'] = df.index.strftime('%Y%m%d').astype(int)
df = df.merge(
adj_factor[['000001.SZ']].reset_index(),
left_on='trade_date',
right_on='index',
how='left'
)
df = df.rename(columns={'000001.SZ': 'adj_factor'})
# 计算后复权价格
df['adj_close'] = df['close'] * df['adj_factor']
print("\n复权后的价格:")
print(df[['close', 'adj_factor', 'adj_close']].tail(10))
finally:
adapter.disconnect()
def demo_batch_processing():
"""批量处理示例"""
adapter = create_adapter(
username='your_username',
password='your_password',
host='your_host',
port=8080
)
if not adapter.connect():
return
try:
# 获取沪深300成分股
index_constituents = adapter.get_index_constituents(['000300.SH'])
hs300_codes = index_constituents['000300.SH']['CON_CODE'].tolist()
print(f"沪深300成分股数量: {len(hs300_codes)}")
# 批量获取财务数据 (分批处理避免超时)
batch_size = 50
all_balance_sheets = {}
for i in range(0, len(hs300_codes), batch_size):
batch_codes = hs300_codes[i:i+batch_size]
print(f"处理第 {i//batch_size + 1} 批,共 {len(batch_codes)} 只股票")
batch_data = adapter.get_balance_sheet(
codes=batch_codes,
start_date=20240930,
end_date=20240930
)
all_balance_sheets.update(batch_data)
# 合并所有数据
combined_data = []
for code, df in all_balance_sheets.items():
if not df.empty:
df['code'] = code
combined_data.append(df)
if combined_data:
result_df = pd.concat(combined_data, ignore_index=True)
print(f"\n合并后的数据形状: {result_df.shape}")
print(result_df[['code', 'REPORTING_PERIOD', 'TOTAL_ASSETS', 'TOTAL_CUR_ASSETS']].head())
finally:
adapter.disconnect()
if __name__ == "__main__":
print("=" * 60)
print("AmazingData 数据源适配器 - 使用示例")
print("=" * 60)
# 注:实际运行前请替换用户名、密码和服务器地址
print("\n提示: 请先在代码中替换 username, password, host, port 为实际值")
# demo_basic_usage()
# demo_data_processing()
# demo_batch_processing()

@ -0,0 +1,729 @@
-- AmazingData 金融数据服务平台 - 数据库初始化脚本
-- PostgreSQL 15+
-- 执行命令: psql -U postgres -d amazing_data -f init.sql
-- ============================================
-- 1. 创建扩展
-- ============================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- 用于模糊搜索
-- ============================================
-- 2. 用户表
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
is_superuser BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE users IS '系统用户表';
COMMENT ON COLUMN users.password_hash IS 'bcrypt加密的密码';
-- 创建默认管理员用户 (密码: admin123, 请在生产环境修改)
-- 密码通过Python bcrypt生成: bcrypt.hashpw('admin123'.encode(), bcrypt.gensalt())
INSERT INTO users (username, password_hash, is_superuser)
VALUES ('admin', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYA.qGZvKG6G', TRUE)
ON CONFLICT (username) DO NOTHING;
-- ============================================
-- 3. SDK配置表
-- ============================================
CREATE TABLE IF NOT EXISTS sdk_configs (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
username VARCHAR(100) NOT NULL,
password VARCHAR(255) NOT NULL,
host VARCHAR(100) NOT NULL,
port INTEGER NOT NULL DEFAULT 8080,
local_path VARCHAR(255) DEFAULT './amazing_data_cache/',
is_active BOOLEAN DEFAULT TRUE,
is_default BOOLEAN DEFAULT FALSE,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE sdk_configs IS 'AmazingData SDK配置表';
COMMENT ON COLUMN sdk_configs.password IS 'SDK登录密码建议加密存储';
-- 确保只有一个默认配置
CREATE OR REPLACE FUNCTION ensure_single_default_sdk()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.is_default THEN
UPDATE sdk_configs SET is_default = FALSE WHERE id != NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_ensure_single_default_sdk ON sdk_configs;
CREATE TRIGGER trg_ensure_single_default_sdk
BEFORE INSERT OR UPDATE ON sdk_configs
FOR EACH ROW
EXECUTE FUNCTION ensure_single_default_sdk();
-- ============================================
-- 4. 系统配置表
-- ============================================
CREATE TABLE IF NOT EXISTS system_configs (
id SERIAL PRIMARY KEY,
config_key VARCHAR(100) UNIQUE NOT NULL,
config_value TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE system_configs IS '系统配置键值表';
-- 插入默认配置
INSERT INTO system_configs (config_key, config_value, description) VALUES
('cache.default_period', 'daily', '默认K线周期'),
('cache.default_days', '365', '默认查询天数'),
('cache.auto_cleanup_days', '7', '实时数据自动清理天数'),
('cache.batch_size', '100', '批量缓存每次处理代码数'),
('cache.missing_threshold', '0.1', '数据缺失判断阈值(10%)'),
('realtime.subscribe_interval', '1000', '实时订阅推送间隔(ms)'),
('jwt.expire_hours', '24', 'JWT Token过期时间(小时)')
ON CONFLICT (config_key) DO NOTHING;
-- ============================================
-- 5. 股票代码信息表
-- ============================================
CREATE TABLE IF NOT EXISTS stock_info (
id SERIAL PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL,
symbol VARCHAR(100) NOT NULL,
security_status INTEGER,
pre_close DECIMAL(12, 4),
high_limited DECIMAL(12, 4),
low_limited DECIMAL(12, 4),
price_tick DECIMAL(10, 4),
exchange VARCHAR(10), -- SH, SZ, BJ
industry VARCHAR(50),
list_date DATE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE stock_info IS '股票基础信息表';
CREATE INDEX IF NOT EXISTS idx_stock_info_code ON stock_info(code);
CREATE INDEX IF NOT EXISTS idx_stock_info_exchange ON stock_info(exchange);
CREATE INDEX IF NOT EXISTS idx_stock_info_symbol ON stock_info USING gin(symbol gin_trgm_ops);
-- ============================================
-- 6. 期货代码信息表
-- ============================================
CREATE TABLE IF NOT EXISTS future_info (
id SERIAL PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL,
symbol VARCHAR(100) NOT NULL,
underlying VARCHAR(20), -- 标的代码
contract_month VARCHAR(10),
pre_close DECIMAL(12, 4),
high_limited DECIMAL(12, 4),
low_limited DECIMAL(12, 4),
price_tick DECIMAL(10, 4),
exchange VARCHAR(10) DEFAULT 'CFE', -- CFE: 中金所
list_date DATE,
expire_date DATE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE future_info IS '期货基础信息表';
CREATE INDEX IF NOT EXISTS idx_future_info_code ON future_info(code);
CREATE INDEX IF NOT EXISTS idx_future_info_underlying ON future_info(underlying);
-- ============================================
-- 7. 股票日线数据表
-- ============================================
CREATE TABLE IF NOT EXISTS stock_kline_daily (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
adj_factor DECIMAL(12, 6), -- 复权因子
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, trade_date)
);
COMMENT ON TABLE stock_kline_daily IS '股票日线K线数据';
CREATE INDEX IF NOT EXISTS idx_stock_daily_code_date ON stock_kline_daily(code, trade_date);
CREATE INDEX IF NOT EXISTS idx_stock_daily_trade_date ON stock_kline_daily(trade_date);
CREATE INDEX IF NOT EXISTS idx_stock_daily_code ON stock_kline_daily(code);
-- 分区表(按年分区,可选)
-- 如果需要分区,可以创建子表
-- CREATE TABLE stock_kline_daily_2024 PARTITION OF stock_kline_daily
-- FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
-- ============================================
-- 8. 股票分钟数据表
-- ============================================
CREATE TABLE IF NOT EXISTS stock_kline_min (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
period_type VARCHAR(10) NOT NULL CHECK (period_type IN ('min1', 'min5', 'min15', 'min30', 'min60')),
trade_datetime TIMESTAMP NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, period_type, trade_datetime)
);
COMMENT ON TABLE stock_kline_min IS '股票分钟K线数据';
CREATE INDEX IF NOT EXISTS idx_stock_min_code_period_datetime ON stock_kline_min(code, period_type, trade_datetime);
CREATE INDEX IF NOT EXISTS idx_stock_min_trade_datetime ON stock_kline_min(trade_datetime);
-- ============================================
-- 9. 期货日线数据表
-- ============================================
CREATE TABLE IF NOT EXISTS future_kline_daily (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
settle DECIMAL(12, 4),
open_interest BIGINT,
pre_settle DECIMAL(12, 4),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, trade_date)
);
COMMENT ON TABLE future_kline_daily IS '期货日线K线数据';
CREATE INDEX IF NOT EXISTS idx_future_daily_code_date ON future_kline_daily(code, trade_date);
CREATE INDEX IF NOT EXISTS idx_future_daily_trade_date ON future_kline_daily(trade_date);
-- ============================================
-- 10. 期货分钟数据表
-- ============================================
CREATE TABLE IF NOT EXISTS future_kline_min (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
period_type VARCHAR(10) NOT NULL CHECK (period_type IN ('min1', 'min5', 'min15', 'min30', 'min60')),
trade_datetime TIMESTAMP NOT NULL,
open DECIMAL(12, 4) NOT NULL,
high DECIMAL(12, 4) NOT NULL,
low DECIMAL(12, 4) NOT NULL,
close DECIMAL(12, 4) NOT NULL,
volume BIGINT NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
settle DECIMAL(12, 4),
open_interest BIGINT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, period_type, trade_datetime)
);
COMMENT ON TABLE future_kline_min IS '期货分钟K线数据';
CREATE INDEX IF NOT EXISTS idx_future_min_code_period_datetime ON future_kline_min(code, period_type, trade_datetime);
-- ============================================
-- 11. 指数日线数据表
-- ============================================
CREATE TABLE IF NOT EXISTS index_kline_daily (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
open DECIMAL(12, 4),
high DECIMAL(12, 4),
low DECIMAL(12, 4),
close DECIMAL(12, 4),
volume BIGINT,
amount DECIMAL(18, 4),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, trade_date)
);
COMMENT ON TABLE index_kline_daily IS '指数日线数据';
CREATE INDEX IF NOT EXISTS idx_index_daily_code_date ON index_kline_daily(code, trade_date);
-- ============================================
-- 12. 实时快照数据表 (TTL 7天)
-- ============================================
CREATE TABLE IF NOT EXISTS realtime_snapshot (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
security_type VARCHAR(20) NOT NULL, -- stock, future, index, etf, kzz, option
trade_time TIMESTAMP NOT NULL,
pre_close DECIMAL(12, 4),
last DECIMAL(12, 4),
open DECIMAL(12, 4),
high DECIMAL(12, 4),
low DECIMAL(12, 4),
close DECIMAL(12, 4),
volume BIGINT,
amount DECIMAL(18, 4),
-- 盘口数据
ask_price1 DECIMAL(12, 4),
ask_price2 DECIMAL(12, 4),
ask_price3 DECIMAL(12, 4),
ask_price4 DECIMAL(12, 4),
ask_price5 DECIMAL(12, 4),
ask_volume1 INTEGER,
ask_volume2 INTEGER,
ask_volume3 INTEGER,
ask_volume4 INTEGER,
ask_volume5 INTEGER,
bid_price1 DECIMAL(12, 4),
bid_price2 DECIMAL(12, 4),
bid_price3 DECIMAL(12, 4),
bid_price4 DECIMAL(12, 4),
bid_price5 DECIMAL(12, 4),
bid_volume1 INTEGER,
bid_volume2 INTEGER,
bid_volume3 INTEGER,
bid_volume4 INTEGER,
bid_volume5 INTEGER,
-- 期货特有字段
settle DECIMAL(12, 4),
open_interest BIGINT,
pre_settle DECIMAL(12, 4),
average_price DECIMAL(12, 4),
-- 状态
trading_phase_code VARCHAR(10),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL -- 过期时间
);
COMMENT ON TABLE realtime_snapshot IS '实时快照数据自动清理7天前数据';
CREATE INDEX IF NOT EXISTS idx_snapshot_code_time ON realtime_snapshot(code, trade_time);
CREATE INDEX IF NOT EXISTS idx_snapshot_expires ON realtime_snapshot(expires_at);
-- 创建自动清理过期数据的触发器
CREATE OR REPLACE FUNCTION auto_cleanup_expired_snapshots()
RETURNS void AS $$
BEGIN
DELETE FROM realtime_snapshot
WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;
-- ============================================
-- 13. 历史快照数据表
-- ============================================
CREATE TABLE IF NOT EXISTS history_snapshot (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
security_type VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
trade_time TIME NOT NULL,
pre_close DECIMAL(12, 4),
last DECIMAL(12, 4),
open DECIMAL(12, 4),
high DECIMAL(12, 4),
low DECIMAL(12, 4),
close DECIMAL(12, 4),
volume BIGINT,
amount DECIMAL(18, 4),
ask_price1 DECIMAL(12, 4),
ask_volume1 INTEGER,
bid_price1 DECIMAL(12, 4),
bid_volume1 INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, trade_date, trade_time)
);
COMMENT ON TABLE history_snapshot IS '历史快照数据';
CREATE INDEX IF NOT EXISTS idx_hist_snapshot_code_date ON history_snapshot(code, trade_date);
-- ============================================
-- 14. 财务数据表 - 资产负债表
-- ============================================
CREATE TABLE IF NOT EXISTS finance_balance_sheet (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
report_date DATE NOT NULL,
report_type INTEGER, -- 1:年报, 2:中报, 3:季报
statement_type INTEGER, -- 报表类型
-- 资产
total_assets DECIMAL(18, 4),
total_cur_assets DECIMAL(18, 4),
total_noncur_assets DECIMAL(18, 4),
currency_cap DECIMAL(18, 4),
notes_receivable DECIMAL(18, 4),
acct_receivable DECIMAL(18, 4),
inventory DECIMAL(18, 4),
fix_assets DECIMAL(18, 4),
-- 负债
total_liab DECIMAL(18, 4),
total_cur_liab DECIMAL(18, 4),
total_noncur_liab DECIMAL(18, 4),
notes_payable DECIMAL(18, 4),
acct_payable DECIMAL(18, 4),
st_borrowing DECIMAL(18, 4),
lt_loan DECIMAL(18, 4),
-- 权益
tot_share_equity DECIMAL(18, 4),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, report_date, statement_type)
);
COMMENT ON TABLE finance_balance_sheet IS '资产负债表';
CREATE INDEX IF NOT EXISTS idx_balance_code_date ON finance_balance_sheet(code, report_date);
-- ============================================
-- 15. 财务数据表 - 现金流量表
-- ============================================
CREATE TABLE IF NOT EXISTS finance_cash_flow (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
report_date DATE NOT NULL,
report_type INTEGER,
statement_type INTEGER,
net_cash_flows_opera_act DECIMAL(18, 4),
net_cash_flows_inv_act DECIMAL(18, 4),
net_cash_flows_fin_act DECIMAL(18, 4),
net_incr_cash_and_cash_equ DECIMAL(18, 4),
cash_recp_sg_and_rs DECIMAL(18, 4),
cash_pay_goods_services DECIMAL(18, 4),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, report_date, statement_type)
);
COMMENT ON TABLE finance_cash_flow IS '现金流量表';
CREATE INDEX IF NOT EXISTS idx_cashflow_code_date ON finance_cash_flow(code, report_date);
-- ============================================
-- 16. 财务数据表 - 利润表
-- ============================================
CREATE TABLE IF NOT EXISTS finance_income (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
report_date DATE NOT NULL,
report_type INTEGER,
statement_type INTEGER,
tot_opera_rev DECIMAL(18, 4),
opera_rev DECIMAL(18, 4),
tot_opera_cost DECIMAL(18, 4),
opera_profit DECIMAL(18, 4),
total_profit DECIMAL(18, 4),
net_pro_incl_min_int_inc DECIMAL(18, 4),
basic_eps DECIMAL(12, 6),
diluted_eps DECIMAL(12, 6),
rd_exp DECIMAL(18, 4),
selling_exp DECIMAL(18, 4),
admin_exp DECIMAL(18, 4),
fin_exp DECIMAL(18, 4),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, report_date, statement_type)
);
COMMENT ON TABLE finance_income IS '利润表';
CREATE INDEX IF NOT EXISTS idx_income_code_date ON finance_income(code, report_date);
-- ============================================
-- 17. 交易日历表
-- ============================================
CREATE TABLE IF NOT EXISTS trading_calendar (
id SERIAL PRIMARY KEY,
market VARCHAR(10) NOT NULL, -- SH, SZ, BJ, CFE
trade_date DATE NOT NULL,
is_trading_day BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(market, trade_date)
);
COMMENT ON TABLE trading_calendar IS '交易日历';
CREATE INDEX IF NOT EXISTS idx_calendar_market_date ON trading_calendar(market, trade_date);
CREATE INDEX IF NOT EXISTS idx_calendar_date ON trading_calendar(trade_date);
-- ============================================
-- 18. 缓存任务表
-- ============================================
CREATE TABLE IF NOT EXISTS cache_tasks (
id SERIAL PRIMARY KEY,
task_name VARCHAR(200) NOT NULL,
task_type VARCHAR(50) NOT NULL CHECK (task_type IN ('detect_missing', 'cache_data', 'sync_data')),
security_type VARCHAR(20) NOT NULL, -- stock, future, index
period_type VARCHAR(10), -- daily, min1, min5, etc.
start_date DATE NOT NULL,
end_date DATE NOT NULL,
code_list TEXT, -- 逗号分隔的代码列表NULL表示全部
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
progress DECIMAL(5, 2) DEFAULT 0, -- 0-100
total_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
error_count INTEGER DEFAULT 0,
error_message TEXT,
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE
);
COMMENT ON TABLE cache_tasks IS '数据缓存任务';
CREATE INDEX IF NOT EXISTS idx_cache_tasks_status ON cache_tasks(status);
CREATE INDEX IF NOT EXISTS idx_cache_tasks_created ON cache_tasks(created_at);
-- ============================================
-- 19. 缓存任务详情表
-- ============================================
CREATE TABLE IF NOT EXISTS cache_task_details (
id BIGSERIAL PRIMARY KEY,
task_id INTEGER NOT NULL REFERENCES cache_tasks(id) ON DELETE CASCADE,
code VARCHAR(20) NOT NULL,
trade_date DATE NOT NULL,
expected_count INTEGER DEFAULT 0,
actual_count INTEGER DEFAULT 0,
is_missing BOOLEAN DEFAULT FALSE,
status VARCHAR(20) DEFAULT 'pending', -- pending, success, failed, skipped
error_message TEXT,
processed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE cache_task_details IS '缓存任务详细记录';
CREATE INDEX IF NOT EXISTS idx_cache_details_task ON cache_task_details(task_id);
CREATE INDEX IF NOT EXISTS idx_cache_details_code ON cache_task_details(code);
-- ============================================
-- 20. API测试日志表
-- ============================================
CREATE TABLE IF NOT EXISTS api_test_logs (
id BIGSERIAL PRIMARY KEY,
test_name VARCHAR(200) NOT NULL,
api_category VARCHAR(50) NOT NULL, -- base_data, stock, future, realtime, finance, etc.
api_endpoint VARCHAR(200) NOT NULL,
request_method VARCHAR(10) NOT NULL,
request_params JSONB,
response_data JSONB,
status_code INTEGER,
execution_time_ms INTEGER,
is_success BOOLEAN DEFAULT FALSE,
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE api_test_logs IS 'API接口测试日志';
CREATE INDEX IF NOT EXISTS idx_test_logs_category ON api_test_logs(api_category);
CREATE INDEX IF NOT EXISTS idx_test_logs_endpoint ON api_test_logs(api_endpoint);
CREATE INDEX IF NOT EXISTS idx_test_logs_created ON api_test_logs(created_at);
-- ============================================
-- 21. 数据同步日志表
-- ============================================
CREATE TABLE IF NOT EXISTS data_sync_logs (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(20) NOT NULL,
security_type VARCHAR(20) NOT NULL,
period_type VARCHAR(10),
start_date DATE,
end_date DATE,
record_count INTEGER DEFAULT 0,
source VARCHAR(50) DEFAULT 'sdk', -- sdk, manual, import
status VARCHAR(20) DEFAULT 'success',
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE data_sync_logs IS '数据同步日志';
CREATE INDEX IF NOT EXISTS idx_sync_logs_code ON data_sync_logs(code);
CREATE INDEX IF NOT EXISTS idx_sync_logs_created ON data_sync_logs(created_at);
-- ============================================
-- 22. 创建视图 - 数据缓存状态概览
-- ============================================
CREATE OR REPLACE VIEW v_cache_status AS
SELECT
code,
'stock' as security_type,
'daily' as period_type,
COUNT(*) as record_count,
MIN(trade_date) as min_date,
MAX(trade_date) as max_date
FROM stock_kline_daily
GROUP BY code
UNION ALL
SELECT
code,
'future' as security_type,
'daily' as period_type,
COUNT(*) as record_count,
MIN(trade_date) as min_date,
MAX(trade_date) as max_date
FROM future_kline_daily
GROUP BY code;
COMMENT ON VIEW v_cache_status IS '数据缓存状态概览视图';
-- ============================================
-- 23. 创建函数 - 获取交易日列表
-- ============================================
CREATE OR REPLACE FUNCTION get_trading_days(
p_market VARCHAR(10),
p_start_date DATE,
p_end_date DATE
)
RETURNS TABLE (trade_date DATE) AS $$
BEGIN
RETURN QUERY
SELECT tc.trade_date
FROM trading_calendar tc
WHERE tc.market = p_market
AND tc.trade_date BETWEEN p_start_date AND p_end_date
AND tc.is_trading_day = TRUE
ORDER BY tc.trade_date;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION get_trading_days IS '获取指定区间的交易日列表';
-- ============================================
-- 24. 创建函数 - 计算数据缺失率
-- ============================================
CREATE OR REPLACE FUNCTION calculate_missing_ratio(
p_code VARCHAR(20),
p_security_type VARCHAR(20),
p_period_type VARCHAR(10),
p_start_date DATE,
p_end_date DATE
)
RETURNS TABLE (
expected_count INTEGER,
actual_count INTEGER,
missing_count INTEGER,
missing_ratio DECIMAL(5, 4)
) AS $$
DECLARE
v_expected INTEGER;
v_actual INTEGER;
v_market VARCHAR(10);
BEGIN
-- 确定市场
v_market := CASE
WHEN p_security_type = 'future' THEN 'CFE'
WHEN RIGHT(p_code, 3) = '.SH' THEN 'SH'
WHEN RIGHT(p_code, 3) = '.SZ' THEN 'SZ'
WHEN RIGHT(p_code, 3) = '.BJ' THEN 'BJ'
ELSE 'SH'
END;
-- 计算期望数据条数
SELECT COUNT(*) INTO v_expected
FROM get_trading_days(v_market, p_start_date, p_end_date);
-- 计算实际数据条数
IF p_security_type = 'stock' AND p_period_type = 'daily' THEN
SELECT COUNT(*) INTO v_actual
FROM stock_kline_daily
WHERE code = p_code AND trade_date BETWEEN p_start_date AND p_end_date;
ELSIF p_security_type = 'future' AND p_period_type = 'daily' THEN
SELECT COUNT(*) INTO v_actual
FROM future_kline_daily
WHERE code = p_code AND trade_date BETWEEN p_start_date AND p_end_date;
ELSE
v_actual := 0;
END IF;
RETURN QUERY
SELECT
v_expected,
v_actual,
GREATEST(0, v_expected - v_actual),
CASE
WHEN v_expected > 0 THEN (v_expected - v_actual)::DECIMAL / v_expected
ELSE 0
END;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION calculate_missing_ratio IS '计算指定代码在指定区间的数据缺失率';
-- ============================================
-- 25. 创建触发器 - 自动更新updated_at
-- ============================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 为用户表创建触发器
DROP TRIGGER IF EXISTS trg_users_updated_at ON users;
CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 为SDK配置表创建触发器
DROP TRIGGER IF EXISTS trg_sdk_configs_updated_at ON sdk_configs;
CREATE TRIGGER trg_sdk_configs_updated_at
BEFORE UPDATE ON sdk_configs
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 为系统配置表创建触发器
DROP TRIGGER IF EXISTS trg_system_configs_updated_at ON system_configs;
CREATE TRIGGER trg_system_configs_updated_at
BEFORE UPDATE ON system_configs
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- 26. 创建分区表(可选,用于大数据量场景)
-- ============================================
-- 如果需要按月分区,可以使用以下命令创建分区表结构
-- 注意这需要PostgreSQL 10+
/*
-- 股票分钟数据按月分区示例
CREATE TABLE stock_kline_min_partitioned (
LIKE stock_kline_min INCLUDING ALL
) PARTITION BY RANGE (trade_datetime);
-- 创建各月分区
CREATE TABLE stock_kline_min_202401 PARTITION OF stock_kline_min_partitioned
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE stock_kline_min_202402 PARTITION OF stock_kline_min_partitioned
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- ... 更多分区
*/
-- ============================================
-- 初始化完成
-- ============================================
SELECT 'Database initialization completed successfully!' as status;

@ -0,0 +1,64 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: amazing_data_postgres
environment:
POSTGRES_DB: amazing_data
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
- ../database/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
networks:
- amazing_data_network
redis:
image: redis:7-alpine
container_name: amazing_data_redis
ports:
- "6379:6379"
networks:
- amazing_data_network
backend:
build:
context: ../backend
dockerfile: Dockerfile
container_name: amazing_data_backend
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/amazing_data
REDIS_URL: redis://redis:6379/0
SECRET_KEY: your-secret-key-change-in-production
DEBUG: "false"
ports:
- "8000:8000"
depends_on:
- postgres
- redis
networks:
- amazing_data_network
restart: unless-stopped
frontend:
build:
context: ../frontend
dockerfile: Dockerfile
container_name: amazing_data_frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- amazing_data_network
restart: unless-stopped
volumes:
postgres_data:
networks:
amazing_data_network:
driver: bridge

@ -0,0 +1,577 @@
# AmazingData 金融数据服务平台 - API接口文档
## 基础信息
- **Base URL**: `http://localhost:8000/api/v1`
- **Content-Type**: `application/json`
- **认证方式**: JWT Bearer Token
## 认证相关
### 1. 用户登录
```http
POST /auth/login
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 86400
}
}
```
### 2. 获取当前用户
```http
GET /auth/me
Authorization: Bearer {token}
```
## 配置管理
### 3. 获取SDK配置列表
```http
GET /configs/sdk
Authorization: Bearer {token}
```
### 4. 创建SDK配置
```http
POST /configs/sdk
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "银河证券生产环境",
"username": "your_username",
"password": "your_password",
"host": "xxx.xxx.xxx.xxx",
"port": 8080,
"local_path": "./amazing_data_cache/",
"is_default": true
}
```
### 5. 测试SDK连接
```http
POST /configs/sdk/{id}/test
Authorization: Bearer {token}
```
## 基础数据
### 6. 获取代码列表
```http
GET /base/codes?security_type=EXTRA_STOCK_A
Authorization: Bearer {token}
```
**参数**:
- `security_type`: 证券类型
- `EXTRA_STOCK_A` - 沪深A股
- `EXTRA_FUTURE` - 期货
- `EXTRA_ETF` - ETF
- `EXTRA_INDEX_A` - 指数
### 7. 获取证券信息
```http
GET /base/codes/{code}/info
Authorization: Bearer {token}
```
### 8. 获取交易日历
```http
GET /base/calendar?market=SH&start_date=20240101&end_date=20241231
Authorization: Bearer {token}
```
## 股票数据
### 9. 获取股票K线数据
```http
GET /stock/kline?codes=000001.SZ&start_date=20240101&end_date=20241231&period=daily
Authorization: Bearer {token}
```
**参数**:
- `codes`: 股票代码,多个用逗号分隔
- `start_date`: 开始日期 (YYYYMMDD)
- `end_date`: 结束日期 (YYYYMMDD)
- `period`: 周期 (daily, min1, min5, min15, min30, min60)
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"000001.SZ": [
{
"trade_date": "2024-01-02",
"open": 10.50,
"high": 10.80,
"low": 10.40,
"close": 10.65,
"volume": 1234567,
"amount": 12845678.90
}
]
}
}
```
### 10. 获取股票K线图数据
```http
GET /stock/kline/{code}/chart?start_date=20240101&end_date=20241231&period=daily
Authorization: Bearer {token}
```
**响应** (ECharts格式):
```json
{
"code": 200,
"message": "success",
"data": {
"categoryData": ["2024-01-02", "2024-01-03", ...],
"values": [
[10.50, 10.80, 10.40, 10.65, 1234567],
[10.65, 10.70, 10.50, 10.60, 987654],
...
],
"volumes": [
[0, 1234567, 1],
[1, 987654, -1],
...
]
}
}
```
### 11. 批量获取股票K线
```http
POST /stock/kline/batch
Authorization: Bearer {token}
Content-Type: application/json
{
"codes": ["000001.SZ", "600000.SH"],
"start_date": "20240101",
"end_date": "20241231",
"period": "daily"
}
```
## 期货数据
### 12. 获取期货K线数据
```http
GET /future/kline?codes=IF2501.CFE&start_date=20240101&end_date=20241231&period=daily
Authorization: Bearer {token}
```
### 13. 获取期货K线图数据
```http
GET /future/kline/{code}/chart?start_date=20240101&end_date=20241231&period=daily
Authorization: Bearer {token}
```
## 实时数据
### 14. 获取最新快照
```http
GET /realtime/snapshot?codes=000001.SZ,600000.SH
Authorization: Bearer {token}
```
### 15. 开始实时订阅
```http
POST /realtime/subscribe
Authorization: Bearer {token}
Content-Type: application/json
{
"codes": ["000001.SZ", "600000.SH"],
"types": ["snapshot"],
"callback_url": "ws://localhost:8000/api/v1/realtime/stream"
}
```
### 16. WebSocket实时数据流
```
WS /realtime/stream?codes=000001.SZ,600000.SH&types=snapshot
Authorization: Bearer {token}
```
**消息格式**:
```json
{
"type": "snapshot",
"code": "000001.SZ",
"data": {
"trade_time": "2025-01-15T10:30:00",
"last": 10.50,
"open": 10.30,
"high": 10.60,
"low": 10.20,
"volume": 1234567,
"amount": 12845678.90
}
}
```
## 财务数据
### 17. 获取资产负债表
```http
GET /finance/balance-sheet?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"000001.SZ": [
{
"report_date": "2024-09-30",
"total_assets": 123456789012.34,
"total_cur_assets": 98765432109.87,
"total_liab": 87654321098.76,
"tot_share_equity": 35802467913.58
}
]
}
}
```
### 18. 获取现金流量表
```http
GET /finance/cash-flow?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
### 19. 获取利润表
```http
GET /finance/income?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
### 20. 获取业绩快报
```http
GET /finance/profit-express?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
### 21. 获取业绩预告
```http
GET /finance/profit-notice?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
## 股东数据
### 22. 获取十大股东
```http
GET /shareholder/top10?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
### 23. 获取股东户数
```http
GET /shareholder/count?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
### 24. 获取股本结构
```http
GET /shareholder/equity?codes=000001.SZ&start_date=20240930&end_date=20240930
Authorization: Bearer {token}
```
## 融资融券
### 25. 获取融资融券汇总
```http
GET /margin/summary?start_date=20240101&end_date=20241231
Authorization: Bearer {token}
```
### 26. 获取融资融券明细
```http
GET /margin/detail?codes=000001.SZ&start_date=20240101&end_date=20241231
Authorization: Bearer {token}
```
## 指数数据
### 27. 获取指数成分股
```http
GET /index/constituents?codes=000300.SH
Authorization: Bearer {token}
```
### 28. 获取指数权重
```http
GET /index/weights?codes=000300.SH&start_date=20240101&end_date=20241231
Authorization: Bearer {token}
```
## ETF数据
### 29. 获取ETF申赎数据
```http
GET /etf/pcf?codes=510050.SH
Authorization: Bearer {token}
```
### 30. 获取ETF份额
```http
GET /etf/share?codes=510050.SH&start_date=20240101&end_date=20241231
Authorization: Bearer {token}
```
## 可转债数据
### 31. 获取可转债发行数据
```http
GET /kzz/issuance?codes=128XXX.SZ
Authorization: Bearer {token}
```
## 缓存管理
### 32. 检测缺失数据
```http
POST /cache/detect-missing
Authorization: Bearer {token}
Content-Type: application/json
{
"security_type": "stock",
"period_type": "daily",
"start_date": "20240101",
"end_date": "20241231",
"code_list": ["000001.SZ", "600000.SH"]
}
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"task_id": 1,
"total_codes": 2,
"missing_codes": [
{
"code": "000001.SZ",
"missing_dates": [
{
"date": "2024-06-01",
"expected": 1,
"actual": 0,
"missing_ratio": 1.0
}
]
}
]
}
}
```
### 33. 批量缓存数据
```http
POST /cache/batch-cache
Authorization: Bearer {token}
Content-Type: application/json
{
"security_type": "stock",
"period_type": "daily",
"start_date": "20240101",
"end_date": "20241231",
"code_list": ["000001.SZ", "600000.SH"]
}
```
### 34. 获取缓存任务列表
```http
GET /cache/tasks?page=1&page_size=20
Authorization: Bearer {token}
```
### 35. 获取缓存任务详情
```http
GET /cache/tasks/{task_id}
Authorization: Bearer {token}
```
### 36. 取消缓存任务
```http
DELETE /cache/tasks/{task_id}
Authorization: Bearer {token}
```
### 37. 获取代码缓存状态
```http
GET /cache/status/{code}?security_type=stock&period_type=daily
Authorization: Bearer {token}
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": {
"code": "000001.SZ",
"security_type": "stock",
"period_type": "daily",
"record_count": 242,
"min_date": "2024-01-02",
"max_date": "2024-12-31",
"missing_ratio": 0.0
}
}
```
## 测试中心
### 38. 获取测试分类
```http
GET /test/categories
Authorization: Bearer {token}
```
**响应**:
```json
{
"code": 200,
"message": "success",
"data": [
{"key": "base_data", "name": "基础数据"},
{"key": "stock", "name": "股票数据"},
{"key": "future", "name": "期货数据"},
{"key": "realtime", "name": "实时数据"},
{"key": "finance", "name": "财务数据"},
{"key": "shareholder", "name": "股东数据"},
{"key": "margin", "name": "融资融券"},
{"key": "index", "name": "指数数据"},
{"key": "etf", "name": "ETF数据"},
{"key": "kzz", "name": "可转债数据"}
]
}
```
### 39. 获取接口列表
```http
GET /test/endpoints?category=stock
Authorization: Bearer {token}
```
### 40. 执行单个接口测试
```http
POST /test/run
Authorization: Bearer {token}
Content-Type: application/json
{
"endpoint": "/api/v1/stock/kline",
"method": "GET",
"params": {
"codes": "000001.SZ",
"start_date": "20240101",
"end_date": "20241231",
"period": "daily"
}
}
```
### 41. 执行全部接口测试
```http
POST /test/run-all
Authorization: Bearer {token}
Content-Type: application/json
{
"categories": ["stock", "future", "finance"]
}
```
### 42. 获取测试历史
```http
GET /test/history?page=1&page_size=20
Authorization: Bearer {token}
```
## 错误码
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 参数错误 |
| 401 | 未授权 |
| 403 | 禁止访问 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
| 1001 | SDK连接失败 |
| 1002 | 数据不存在 |
| 1003 | 任务执行失败 |
## 数据类型说明
### K线数据结构
| 字段 | 类型 | 说明 |
|------|------|------|
| trade_date | string | 交易日期 (YYYY-MM-DD) |
| trade_datetime | string | 交易时间 (YYYY-MM-DD HH:MM:SS) |
| open | number | 开盘价 |
| high | number | 最高价 |
| low | number | 最低价 |
| close | number | 收盘价 |
| volume | number | 成交量 |
| amount | number | 成交金额 |
### 期货K线额外字段
| 字段 | 类型 | 说明 |
|------|------|------|
| settle | number | 结算价 |
| open_interest | number | 持仓量 |
### 快照数据结构
| 字段 | 类型 | 说明 |
|------|------|------|
| code | string | 证券代码 |
| trade_time | string | 交易时间 |
| last | number | 最新价 |
| open | number | 开盘价 |
| high | number | 最高价 |
| low | number | 最低价 |
| volume | number | 成交量 |
| amount | number | 成交金额 |
| ask_price1-5 | number | 卖1-5价格 |
| ask_volume1-5 | number | 卖1-5数量 |
| bid_price1-5 | number | 买1-5价格 |
| bid_volume1-5 | number | 买1-5数量 |
---
**文档版本**: 1.0
**更新日期**: 2025年

@ -0,0 +1,27 @@
# 前端Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# 复制package.json
COPY package.json ./
RUN npm install
# 复制源代码
COPY . .
# 构建
RUN npm run build
# 生产环境
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AmazingData金融数据服务平台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

@ -0,0 +1,17 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://host.docker.internal:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

@ -0,0 +1,17 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,31 @@
{
"name": "amazing-data-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"axios": "^1.6.2",
"element-plus": "^2.4.4",
"echarts": "^5.4.3",
"vue-echarts": "^6.6.1",
"@element-plus/icons-vue": "^2.1.0",
"dayjs": "^1.11.10",
"js-cookie": "^3.0.5"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/js-cookie": "^3.0.6",
"@vitejs/plugin-vue": "^4.5.0",
"typescript": "^5.3.2",
"vite": "^5.0.4",
"vue-tsc": "^1.8.22"
}
}

@ -0,0 +1,26 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
width: 100%;
height: 100vh;
}
</style>

@ -0,0 +1,13 @@
import request from '@/utils/request'
export const login = (data: { username: string; password: string }) => {
return request.post('/auth/login', data)
}
export const getUserInfo = () => {
return request.get('/auth/me')
}
export const logout = () => {
return request.post('/auth/logout')
}

@ -0,0 +1,40 @@
import request from '@/utils/request'
export const detectMissingData = (data: {
security_type: string
period_type: string
start_date: string
end_date: string
code_list: string[]
}) => {
return request.post('/cache/detect-missing', data)
}
export const batchCacheData = (data: {
security_type: string
period_type: string
start_date: string
end_date: string
code_list: string[]
}) => {
return request.post('/cache/batch-cache', data)
}
export const getCacheTasks = (params?: { page?: number; page_size?: number }) => {
return request.get('/cache/tasks', { params })
}
export const getCacheTask = (taskId: number) => {
return request.get(`/cache/tasks/${taskId}`)
}
export const cancelCacheTask = (taskId: number) => {
return request.delete(`/cache/tasks/${taskId}`)
}
export const getCacheStatus = (
code: string,
params: { security_type: string; period_type: string }
) => {
return request.get(`/cache/status/${code}`, { params })
}

@ -0,0 +1,25 @@
import request from '@/utils/request'
export const getSDKConfigs = () => {
return request.get('/configs/sdk')
}
export const createSDKConfig = (data: any) => {
return request.post('/configs/sdk', data)
}
export const updateSDKConfig = (id: number, data: any) => {
return request.put(`/configs/sdk/${id}`, data)
}
export const deleteSDKConfig = (id: number) => {
return request.delete(`/configs/sdk/${id}`)
}
export const testSDKConfig = (id: number) => {
return request.post(`/configs/sdk/${id}/test`)
}
export const setDefaultConfig = (id: number) => {
return request.post(`/configs/sdk/${id}/set-default`)
}

@ -0,0 +1,25 @@
import request from '@/utils/request'
export const getBalanceSheet = (params: {
codes: string
start_date: string
end_date: string
}) => {
return request.get('/finance/balance-sheet', { params })
}
export const getCashFlow = (params: {
codes: string
start_date: string
end_date: string
}) => {
return request.get('/finance/cash-flow', { params })
}
export const getIncome = (params: {
codes: string
start_date: string
end_date: string
}) => {
return request.get('/finance/income', { params })
}

@ -0,0 +1,21 @@
import request from '@/utils/request'
export const getFutureKline = (params: {
codes: string
start_date: string
end_date: string
period?: string
}) => {
return request.get('/future/kline', { params })
}
export const getFutureKlineChart = (
code: string,
params: {
start_date: string
end_date: string
period?: string
}
) => {
return request.get(`/future/kline/${code}/chart`, { params })
}

@ -0,0 +1,30 @@
import request from '@/utils/request'
export const getStockKline = (params: {
codes: string
start_date: string
end_date: string
period?: string
}) => {
return request.get('/stock/kline', { params })
}
export const getStockKlineChart = (
code: string,
params: {
start_date: string
end_date: string
period?: string
}
) => {
return request.get(`/stock/kline/${code}/chart`, { params })
}
export const batchGetStockKline = (data: {
codes: string[]
start_date: string
end_date: string
period?: string
}) => {
return request.post('/stock/kline/batch', data)
}

@ -0,0 +1,29 @@
import request from '@/utils/request'
export const getTestCategories = () => {
return request.get('/test/categories')
}
export const getTestEndpoints = (category?: string) => {
return request.get('/test/endpoints', { params: { category } })
}
export const runSingleTest = (data: {
endpoint: string
method: string
params?: Record<string, any>
}) => {
return request.post('/test/run', data)
}
export const runAllTests = (data: { categories: string[] }) => {
return request.post('/test/run-all', data)
}
export const getTestHistory = (params?: { page?: number; page_size?: number }) => {
return request.get('/test/history', { params })
}
export const getTestDetail = (testId: number) => {
return request.get(`/test/history/${testId}`)
}

@ -0,0 +1,22 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')

@ -0,0 +1,73 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/store/user'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { public: true }
},
{
path: '/',
name: 'Layout',
component: () => import('@/views/Layout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '首页', icon: 'HomeFilled' }
},
{
path: 'data-query',
name: 'DataQuery',
component: () => import('@/views/DataQuery/index.vue'),
meta: { title: '数据查询', icon: 'DataLine' }
},
{
path: 'config',
name: 'ConfigManager',
component: () => import('@/views/ConfigManager.vue'),
meta: { title: '配置管理', icon: 'Setting' }
},
{
path: 'cache',
name: 'CacheManager',
component: () => import('@/views/CacheManager/index.vue'),
meta: { title: '缓存管理', icon: 'Box' }
},
{
path: 'test',
name: 'TestCenter',
component: () => import('@/views/TestCenter/index.vue'),
meta: { title: '测试中心', icon: 'CircleCheck' }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.public) {
next()
return
}
if (!userStore.token) {
next('/login')
return
}
next()
})
export default router

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

@ -0,0 +1,44 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import Cookies from 'js-cookie'
const TOKEN_KEY = 'amazing_data_token'
export const useUserStore = defineStore('user', () => {
// State
const token = ref<string>(Cookies.get(TOKEN_KEY) || '')
const userInfo = ref<any>(null)
// Getters
const isLoggedIn = computed(() => !!token.value)
// Actions
const setToken = (newToken: string) => {
token.value = newToken
Cookies.set(TOKEN_KEY, newToken, { expires: 1 })
}
const clearToken = () => {
token.value = ''
userInfo.value = null
Cookies.remove(TOKEN_KEY)
}
const setUserInfo = (info: any) => {
userInfo.value = info
}
const logout = () => {
clearToken()
}
return {
token,
userInfo,
isLoggedIn,
setToken,
clearToken,
setUserInfo,
logout
}
})

@ -0,0 +1,52 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/user'
// 创建axios实例
const request = axios.create({
baseURL: '/api/v1',
timeout: 30000
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
// 401未授权跳转到登录页
if (res.code === 401) {
const userStore = useUserStore()
userStore.logout()
window.location.href = '/login'
}
return Promise.reject(new Error(res.message))
}
return res
},
(error) => {
const message = error.response?.data?.message || error.message || '网络错误'
ElMessage.error(message)
return Promise.reject(error)
}
)
export default request

@ -0,0 +1,191 @@
<template>
<div class="detect-missing">
<el-card>
<el-form :model="form" label-width="100px">
<el-form-item label="证券类型">
<el-radio-group v-model="form.securityType">
<el-radio-button label="stock">股票</el-radio-button>
<el-radio-button label="future">期货</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="周期">
<el-select v-model="form.periodType" style="width: 120px;">
<el-option label="日线" value="daily" />
<el-option label="1分钟" value="min1" />
<el-option label="5分钟" value="min5" />
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="form.startDate"
type="date"
placeholder="开始日期"
value-format="YYYYMMDD"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="form.endDate"
type="date"
placeholder="结束日期"
value-format="YYYYMMDD"
/>
</el-form-item>
<el-form-item label="代码列表">
<el-input
v-model="codeInput"
type="textarea"
:rows="4"
placeholder="输入代码,每行一个或逗号分隔"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleDetect" :loading="detecting">
<el-icon><Search /></el-icon>
</el-button>
<el-button type="success" @click="handleCache" :loading="caching" :disabled="!hasMissing">
<el-icon><Download /></el-icon>
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="result-card" v-if="detectResult.length > 0">
<template #header>
<span>检测结果</span>
</template>
<el-table :data="detectResult" stripe>
<el-table-column prop="code" label="代码" width="120" />
<el-table-column prop="missingCount" label="缺失天数" width="100" />
<el-table-column label="缺失率">
<template #default="{ row }">
<el-progress
:percentage="Math.round(row.missingRatio * 100)"
:status="row.missingRatio > 0.5 ? 'exception' : 'warning'"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button type="primary" size="small" @click="showDetail(row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { detectMissingData, batchCacheData } from '@/api/cache'
const detecting = ref(false)
const caching = ref(false)
const codeInput = ref('000001.SZ\n600000.SH')
const detectResult = ref<any[]>([])
const taskId = ref<number | null>(null)
const hasMissing = computed(() => detectResult.value.some(r => r.missingCount > 0))
const form = reactive({
securityType: 'stock',
periodType: 'daily',
startDate: getDefaultStartDate(),
endDate: getDefaultEndDate()
})
function getDefaultStartDate() {
const date = new Date()
date.setFullYear(date.getFullYear() - 1)
return formatDate(date)
}
function getDefaultEndDate() {
return formatDate(new Date())
}
function formatDate(date: Date) {
return date.toISOString().slice(0, 10).replace(/-/g, '')
}
const parseCodes = () => {
return codeInput.value
.split(/[\n,]/)
.map(c => c.trim())
.filter(c => c.length > 0)
}
const handleDetect = async () => {
const codes = parseCodes()
if (codes.length === 0) {
ElMessage.warning('请输入代码')
return
}
detecting.value = true
try {
const res: any = await detectMissingData({
security_type: form.securityType,
period_type: form.periodType,
start_date: form.startDate,
end_date: form.endDate,
code_list: codes
})
if (res.data) {
taskId.value = res.data.task_id
detectResult.value = res.data.missing_codes.map((item: any) => ({
code: item.code,
missingCount: item.missing_dates.length,
missingRatio: item.missing_dates.length > 0
? item.missing_dates.reduce((sum: number, d: any) => sum + d.missing_ratio, 0) / item.missing_dates.length
: 0,
details: item.missing_dates
}))
}
} catch (error) {
console.error(error)
} finally {
detecting.value = false
}
}
const handleCache = async () => {
const codes = parseCodes()
if (codes.length === 0) return
caching.value = true
try {
await batchCacheData({
security_type: form.securityType,
period_type: form.periodType,
start_date: form.startDate,
end_date: form.endDate,
code_list: codes
})
ElMessage.success('缓存任务已启动')
} catch (error) {
console.error(error)
} finally {
caching.value = false
}
}
const showDetail = (row: any) => {
//
console.log(row.details)
}
</script>
<style scoped>
.detect-missing {
padding: 10px;
}
.result-card {
margin-top: 20px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save