deng.yinping hai 1 ano
achega
3af2339584
Modificáronse 8 ficheiros con 639 adicións e 0 borrados
  1. 59 0
      app.py
  2. 22 0
      app_config.py
  3. 27 0
      readme.md
  4. 3 0
      requirements.txt
  5. 241 0
      services/mongodb_tools.py
  6. 41 0
      services/quote_query_entity.py
  7. 106 0
      static/css/styles.css
  8. 140 0
      templates/index.html

+ 59 - 0
app.py

@@ -0,0 +1,59 @@
+'''
+Author: deng.yinping deng.yinping@muchinfo.cn
+Date: 2024-11-28 15:57:05
+LastEditors: deng.yinping deng.yinping@muchinfo.cn
+LastEditTime: 2024-11-29 11:01:44
+FilePath: \py_quote\app.py
+Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+'''
+from flask import Flask, render_template, request, redirect, url_for
+import os
+import datetime
+from services.mongodb_tools import MongoDBTools
+from services.quote_query_entity import QuoteQueryEntity 
+from flask import Flask, render_template, request
+from app_config import UPLOAD_FOLDER, SERVICE_PORT, DEFAULT_PARAM
+
+app = Flask(__name__)
+
+app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
+# 从配置文件中读取配置
+app.config['SERVER_PORT'] = SERVICE_PORT  # 可以在配置文件中设置这个值
+
+@app.route('/', methods=['GET', 'POST'])
+def index():
+    file_list = []
+
+    if request.method == 'POST':
+        # 查询mongodb数据并生成excel
+        query_entity = QuoteQueryEntity()
+        query_entity.host = request.form['host']
+        query_entity.port = int(request.form['port'])
+        query_entity.username = request.form['username']
+        query_entity.password = request.form['password']
+        query_entity.db_name = request.form['db_name']
+        query_entity.col_name = request.form['col_name']
+        query_entity.goods_code = request.form['goods_code']
+        query_entity.query_type = int(request.form['query_type'])
+        query_entity.start_time = request.form['start_time'].replace("T", " ")
+        query_entity.end_time = request.form['end_time'].replace("T", " ")
+        query_entity.diff_value = int(request.form['diff_value'])
+        query_entity.record_num = int(request.form['record_num'])
+        query_entity.files_num = int(request.form['files_num'])
+        MongoDBTools.query_mongodb_data(query_entity)
+        
+        file_list = MongoDBTools.read_files(app.config['UPLOAD_FOLDER'], '.xlsx', query_entity.files_num)
+        return render_template('index.html', defaults=query_entity, file_list=file_list)
+    else:
+        # 默认查询条件
+        file_list = MongoDBTools.read_files(app.config['UPLOAD_FOLDER'], '.xlsx', DEFAULT_PARAM.files_num)
+        return render_template('index.html', defaults=DEFAULT_PARAM, file_list=file_list)
+
+@app.route('/open_file/<filename>')
+def open_file(filename):
+    # 返回文件内容
+    return redirect(url_for('static', filename=f'quote_data/{filename}'))
+
+
+if __name__ == '__main__':
+    app.run(host='0.0.0.0', port=app.config['SERVER_PORT'])

+ 22 - 0
app_config.py

@@ -0,0 +1,22 @@
+# 设置查询文件夹路径
+from services.quote_query_entity import QuoteQueryEntity
+
+
+UPLOAD_FOLDER = 'static/quote_data'
+SERVICE_PORT = 8000
+
+DEFAULT_PARAM = QuoteQueryEntity()
+
+DEFAULT_PARAM.host = '192.168.31.204'       # 主机IP
+DEFAULT_PARAM.port = 5025                   # 主机端口号
+DEFAULT_PARAM.username = 'quote_test01'     # mongodb用户名
+DEFAULT_PARAM.password = '123456'           # mongodb用户密码
+DEFAULT_PARAM.db_name = 'HistoryQuote'      # mongodb 数据库名
+DEFAULT_PARAM.col_name = 'quotetik'         # mongodb 集合名称
+DEFAULT_PARAM.goods_code = 'AU01'           # 商品代码
+DEFAULT_PARAM.query_type = 1                # 查询类型 1-按价格点差(买价) # 2-按行情时间(s)
+DEFAULT_PARAM.start_time = ''               # 开始时间# current_date.strftime('%Y-%m-%d') + " 00:00:01",
+DEFAULT_PARAM.end_time = ''                 # 结束时间 # current_date.strftime('%Y-%m-%d') + " 23:59:59",
+DEFAULT_PARAM.diff_value = 10               # 差值(价差、时差)
+DEFAULT_PARAM.record_num = 1000             # 取前N条 - 为 none 或 0时取所有
+DEFAULT_PARAM.files_num = 10                # 文件显示数

+ 27 - 0
readme.md

@@ -0,0 +1,27 @@
+# 项目描述
+行情Tick数据分析工具
+
+## 安装使用
+*python 版本使用3.8.5, 因为行情mongodb版本为3.4.7, pymongo库连接有版本依赖* 
+1. 安装依赖包
+    ```
+    bash
+    pip install -r requirements.txt
+
+2. 应用配置
+    ```
+   app_config.py文件中配置相关默认值
+
+3. 启动项目
+   ```
+   bash
+   python app.py
+
+
+
+## 开发说明
+1. 使用 pipreqs 生成精简的 requirements.txt
+    ```
+    pip install pipreqs
+
+    pipreqs ./ --force

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+Flask==3.1.0
+openpyxl==3.1.5
+pymongo==3.13.0

+ 241 - 0
services/mongodb_tools.py

@@ -0,0 +1,241 @@
+import pymongo
+import openpyxl
+from openpyxl.styles import PatternFill
+import os
+import platform
+from datetime import datetime
+
+from services.quote_query_entity import QuoteQueryEntity
+ 
+class MongoDBTools:
+    
+    @staticmethod
+    def query_mongodb_data(query_entity: QuoteQueryEntity):
+        if query_entity is None:
+            print("param error")
+            return 
+        try:
+            tools = MongoDBTools()
+            client = tools.connect_mongodb(query_entity)
+            if client:
+                # 查询行情数据
+                records, diff_records = tools.get_quote_data_by_type(client, query_entity)
+                
+                # 导出数据到excel
+                # 默认为按价格
+                file_name_pre = "static\quote_data\\price_";
+                if query_entity.query_type == 2:
+                    # 按时间
+                    file_name_pre = "static\quote_data\\time_";
+                if records is not None and len(records) > 0:
+                    tools.export_to_excel(records, diff_records, query_entity.goods_code, file_name_pre)
+                print("time diff count: " + str(int(len(diff_records) /2)))
+                
+                client.close()
+        except Exception as e:
+            print(f"MongoDB 查询失败:{e}")
+    
+    @staticmethod    
+    def read_files(file_folder, file_extend, limit_num):
+        # 处理表单数据,如果需要做查询操作
+        # 查询文件夹中的 xlsx 文件并按生成时间倒序排列
+        files = []
+        for filename in os.listdir(file_folder):
+            if filename.endswith(file_extend):
+                file_path = os.path.join(file_folder, filename)
+                created_time = os.path.getmtime(file_path)
+                files.append({
+                    'filename': filename,
+                    'created_time': datetime.fromtimestamp(created_time).strftime('%Y-%m-%d %H:%M:%S')
+                })
+
+        # 按照文件创建时间倒序排序
+        file_list = sorted(files, key=lambda x: x['created_time'], reverse=True)
+        
+        # 取出最新的N个文件
+        recent_files = file_list[:limit_num]
+        
+        return recent_files
+    
+    def connect_mongodb(self, query_entity: QuoteQueryEntity):
+        # 创建 MongoDB 连接 URI
+        uri = f"mongodb://{query_entity.username}:{query_entity.password}@{query_entity.host}:{query_entity.port}/{query_entity.db_name}"
+        try:
+            # 替换为你的 MongoDB 连接字符串
+            # 默认本地运行的 MongoDB 连接地址
+            client = pymongo.MongoClient(uri)
+            print("连接 MongoDB 成功!")
+            return client
+        except Exception as e:
+            print(f"连接 MongoDB 失败:{e}")
+            return None
+
+    def get_quote_data(self, client, query_entity: QuoteQueryEntity):
+        try:
+            # 选择数据库(如果不存在,则会自动创建)
+            db = client[query_entity.db_name]
+
+            # 选择集合(类似关系型数据库中的表)
+            collection = db[query_entity.col_name]
+            query = {}
+            
+            if query_entity.start_time is not None and len(query_entity.start_time) > 0 and query_entity.end_time is not None and len(query_entity.end_time) > 0:
+                query = {
+                    "GC": query_entity.goods_code,
+                    "SAT": {
+                        "$gte": query_entity.start_time,  # Greater than or equal to start_time
+                        "$lte": query_entity.end_time     # Less than or equal to end_time
+                    }
+                }
+            else:
+                query = {
+                    "GC": query_entity.goods_code
+                }
+
+            latest_records = None
+            # 查询记录 record_num = 0 或 none ,取所有记录
+            if query_entity.record_num is None or query_entity.record_num == 0:
+                latest_records = list(
+                    collection.find(query).sort("_id", -1)
+                )
+            # record_num > 0, 取最新的N条
+            if query_entity.record_num is not None and query_entity.record_num > 0:
+                    latest_records = list(
+                    collection.find(query).sort("_id", -1)
+                    .limit(query_entity.record_num)      # 取最新 N 条
+                )
+
+            return latest_records
+        except Exception as e:
+            print(f"数据库操作失败:{e}")
+        
+    def get_quote_data_by_type(self, client, query_entity: QuoteQueryEntity):
+        try:
+            latest_records = self.get_quote_data(client, query_entity)
+            
+            if latest_records is None or len(latest_records) == 0:
+                print("no records!")
+                return None, None
+
+            # 初始化变量
+            previous = None
+            previous_bid = None
+            previous_sat = None
+            # 定义时间格式
+            sta_format = "%Y-%m-%d %H:%M:%S"
+            diff_records = []
+            
+            # 遍历记录,查找 BID 差值绝对值大于 500 的记录
+            for record in latest_records:
+                # print("record info:", record)
+                current_bid = record.get("Bid")
+                current_sat = record.get("SAT")
+                record["Color"] = '0'
+                if query_entity.query_type == 1:
+                    # 1: 按价差(买价)
+                    if current_bid is not None and previous_bid is not None:
+                        difference = abs(current_bid - previous_bid)
+                        if abs(difference) > query_entity.diff_value:
+                            previous["Color"] = "1"
+                            record["Color"] = "1"
+                            diff_records.append(previous)
+                            diff_records.append(record)
+                elif query_entity.query_type == 2:
+                    # 2-按时间差
+                    if current_sat is not None and previous_sat is not None:
+                        try:
+                            pre_sta_date = datetime.strptime(str(previous_sat), sta_format)
+                            cur_sta_date = datetime.strptime(current_sat, sta_format)
+                            difference = (cur_sta_date - pre_sta_date).total_seconds()
+                            if abs(difference) > query_entity.diff_value:
+                                previous["Color"] = "1"
+                                record["Color"] = "1"
+                                diff_records.append(previous)
+                                diff_records.append(record)
+                        except Exception as e:
+                            continue
+                            
+                previous = record
+                previous_bid = current_bid
+                previous_sat = current_sat
+            
+            return latest_records, diff_records
+        except Exception as e:
+            print(f"数据库操作失败:{e}") 
+    
+    def export_to_excel(self, records, diff_records, goods_code, file_name_pre):
+        if records is None:
+            return
+        
+        # 创建一个 Excel 文件
+        wb = openpyxl.Workbook()
+        ws = wb.active
+        ws.title = "Full Data"
+        
+        # 更新样式
+        ws = self.update_sheet_style(ws, records)
+
+        # 添加sheet2
+        if diff_records is not None and len(diff_records) > 0:
+            # 创建 sheet2 并填充数据
+            ws_filter = wb.create_sheet('Filter Data')  # 创建新工作表 'Sheet2'
+            ws_filter = self.update_sheet_style(ws_filter, diff_records)
+            
+            # 设置第二个工作表为默认激活工作表
+            wb.active = 1  # 激活 'Sheet2',index 从 0 开始,1 表示第二个工作表
+                 
+        # 保存 Excel 文件
+        file_name = file_name_pre +  goods_code + "_" + datetime.now().strftime("%Y%m%d%H%M%S") + ".xlsx"
+        wb.save(file_name)
+        
+        print("quote date export to:" + file_name)
+        
+        # 打开excel文件
+        # open_excel(file_name)
+    
+    def update_sheet_style(self, ws, records):
+        if ws is None:
+            return None
+        
+        # 设置黄色标记的填充样式
+        yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
+        
+         # 写入表头
+        ws.append(["GC", "SAT", "PE", "Bid", "Ask", "Color"])
+        
+        # 设置列宽
+        ws.column_dimensions['A'].width = 20  # GC 列的宽度
+        ws.column_dimensions['B'].width = 30  # SAT 列的宽度
+        ws.column_dimensions['C'].width = 20  # PE 列的宽度
+        ws.column_dimensions['D'].width = 20  # Bid 列的宽度
+        ws.column_dimensions['E'].width = 20  # Ask 列的宽度
+        ws.column_dimensions['F'].width = 20  # Color 列的宽度
+        
+        # 启用筛选功能
+        ws.auto_filter.ref = ws.dimensions  # 激活自动筛选
+        
+                # 写入数据并根据 color 属性设置行颜色
+        for row in records:
+            row_values = [row["GC"], row["SAT"], row["PE"], row["Bid"], row["Ask"], row["Color"]]
+            ws.append(row_values)
+            
+            # 如果 color 是 "yellow",则标记该行的颜色为黄色
+            if row["Color"] == "1":
+                # 获取当前行的行号
+                row_num = ws.max_row
+                # 为当前行的所有单元格设置背景颜色
+                for cell in ws[row_num]:
+                    cell.fill = yellow_fill
+                    
+        return ws
+    
+    # 自动打开 Excel 文件
+    def open_excel(self, file_path):
+        system_name = platform.system()
+        
+        if system_name == "Windows":
+            os.startfile(file_path)  # 在 Windows 上使用 os.startfile 打开文件
+        elif system_name == "Darwin":  # macOS
+            os.system(f"open {file_path}")
+        elif system_name == "Linux":
+            os.system(f"xdg-open {file_path}")  # 在 Linux 上使用 xdg-open 打开文件

+ 41 - 0
services/quote_query_entity.py

@@ -0,0 +1,41 @@
+'''
+Author: deng.yinping deng.yinping@muchinfo.cn
+Date: 2024-11-28 13:58:49
+LastEditors: deng.yinping deng.yinping@muchinfo.cn
+LastEditTime: 2024-11-29 11:14:11
+FilePath: \py_quote\models\quote_query_entity.py
+Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+'''
+from dataclasses import dataclass
+
+@dataclass
+class QuoteQueryEntity:
+    # 主机IP
+    host: str
+    # 主机端口号
+    port: int
+    # mongodb用户名
+    username: str
+    # mongodb用户密码
+    password: str
+    # mongodb 数据库名
+    db_name: str
+    # mongodb 集合名称
+    col_name: str
+    # 商品代码
+    goods_code: str
+    # 开始时间
+    start_time: str
+    # 结束时间
+    end_time: str
+    # 查询类型 1-按价格点差(买价) # 2-按行情时间(s)
+    query_type: int 
+    # 差值(价差、时差)
+    diff_value: int
+    # 取前N条 - 为 none 或 0时取所有
+    record_num: int
+    # 文件显示数
+    files_num: int
+    
+    def __init__(self):
+        pass

+ 106 - 0
static/css/styles.css

@@ -0,0 +1,106 @@
+/* 基本样式 */
+
+body {
+    font-family: Arial, sans-serif;
+    margin: 0;
+    padding: 0;
+    background-color: #f4f4f4;
+}
+
+
+/* 表单容器 */
+
+.form-container {
+    width: 90%;
+    margin: 0 auto;
+    padding: 20px;
+    background-color: #ffffff;
+    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
+}
+
+
+/* 表单行,使用 flexbox 布局 */
+
+.form-row {
+    display: flex;
+    justify-content: space-between;
+    /* 水平分配空间 */
+    margin-bottom: 20px;
+    flex-wrap: wrap;
+    /* 如果窗口过小,允许换行 */
+}
+
+
+/* 每个查询条件的容器 */
+
+.form-group {
+    display: flex;
+    align-items: center;
+    /* 垂直居中 */
+    flex: 0 0 30%;
+    /* 每个查询条件占宽度的 30%,留出空隙 */
+}
+
+
+/* 标签 */
+
+label {
+    display: inline-block;
+    width: 100px;
+    /* 固定标签宽度 */
+    font-weight: bold;
+    text-align: right;
+    margin-right: 10px;
+    /* 标签与输入框之间的距离 */
+    white-space: nowrap;
+    /* 防止标签换行 */
+}
+
+
+/* 输入框和下拉框样式 */
+
+input,
+select {
+    padding: 8px;
+    width: 100%;
+    /* 输入框宽度占满剩余空间 */
+    box-sizing: border-box;
+}
+
+
+/* 提交按钮 */
+
+button {
+    padding: 12px 20px;
+    background-color: #4CAF50;
+    color: white;
+    border: none;
+    cursor: pointer;
+    width: 100%;
+    font-size: 16px;
+}
+
+button:hover {
+    background-color: #45a049;
+}
+
+
+/* 每行之间的间距 */
+
+.form-row+.form-row {
+    margin-top: 10px;
+}
+
+
+/* 样式调整,保证响应式 */
+
+@media (max-width: 768px) {
+    .form-group {
+        flex: 0 0 100%;
+        /* 在小屏幕上,每个查询条件占满整行 */
+        margin-bottom: 10px;
+    }
+    label {
+        width: 80px;
+    }
+}

+ 140 - 0
templates/index.html

@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>行情校验工具</title>
+    <!-- 引入外部 CSS 文件 -->
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
+
+    <!-- 引入 flatpickr 的 CSS 和 JavaScript 文件 -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
+    <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
+
+</head>
+<body>
+    <div class="form-container">
+        <h1>行情校验工具</h1>
+        <form method="POST">
+            <!-- 第一行:三个查询条件 -->
+            <div class="form-row">
+                <div class="form-group">
+                    <label for="host">主机:</label>
+                    <input type="text" id="host" name="host" value="{{ defaults['host'] }}" required>
+                </div>
+                <div class="form-group">
+                    <label for="port">端口:</label>
+                    <input type="text" id="port" name="port" value="{{ defaults['port'] }}" required>
+                </div>
+                <div class="form-group">
+                    <label for="username">用户名:</label>
+                    <input type="text" id="username" name="username" value="{{ defaults['username'] }}" required>
+                </div>
+            </div>
+
+            <!-- 第二行:三个查询条件 -->
+            <div class="form-row">
+                <div class="form-group">
+                    <label for="password">密码:</label>
+                    <input type="text" id="password" name="password" value="{{ defaults['password'] }}" required>
+                </div>
+                <div class="form-group">
+                    <label for="db_name">数据库名:</label>
+                    <input type="text" id="db_name" name="db_name" value="{{ defaults['db_name'] }}" required>
+                </div>
+                <div class="form-group">
+                    <label for="col_name">集合名称:</label>
+                    <input type="text" id="col_name" name="col_name" value="{{ defaults['col_name'] }}" required>
+                </div>
+            </div>
+
+            <!-- 第三行:三个查询条件 -->
+            <div class="form-row">
+                <div class="form-group">
+                    <label for="goods_code">商品代码:</label>
+                    <input type="text" id="goods_code" name="goods_code" value="{{ defaults['goods_code'] }}" required>
+                </div>
+                <div class="form-group">
+                    <label for="query_type">校验类型:</label>
+                    <select id="query_type" name="query_type" required>
+                        <option value="1" {% if defaults['query_type'] == '1' %}selected{% endif %}>按价格点差(买价)</option>
+                        <option value="2" {% if defaults['query_type'] == '2' %}selected{% endif %}>按行情时间(s)</option>
+                    </select>
+                </div>
+                <div class="form-group">
+                    <label for="diff_value">差值:</label>
+                    <input type="text" id="diff_value" name="diff_value" value="{{ defaults['diff_value'] }}" required>
+                </div>
+            </div>
+
+            <!-- 第四行:三个查询条件 -->
+            <div class="form-row">
+                <div class="form-group">
+                    <label for="record_num">最新记录数:</label>
+                    <input type="text" id="record_num" name="record_num" value="{{ defaults['record_num'] }}" required>
+                </div>
+                <div class="form-group">
+                    <label for="start_time">开始时间:</label>
+                    <input type="text" id="start_time" name="start_time" value="{{ defaults['start_time'] }}" >
+                </div>
+                <div class="form-group">
+                    <label for="end_time">结束时间:</label>
+                    <input type="text" id="end_time" name="end_time" value="{{ defaults['end_time'] }}" >
+                </div>
+            </div>
+
+            <div class="form-row">
+                <div class="form-group">
+                    <label for="files_num">显示文件数:</label>
+                    <input type="text" id="files_num" name="files_num" value="{{ defaults['files_num'] }}" required>
+                </div>
+                <!-- 提交按钮 -->
+                <div class="form-group">
+                    <button type="submit">查询</button>
+                </div>
+                <div class="form-group">
+
+                </div>
+            </div>
+        </form>
+
+        {% if file_list %}
+        <div class="file-list">
+            <h2>查询结果:</h2>
+            <ul>
+                {% for file in file_list %}
+                    <li>
+                        <a href="{{ url_for('open_file', filename=file['filename']) }}" target="_blank">{{ file['filename'] }} - {{ file['created_time'] }}</a>
+                    </li>
+                {% endfor %}
+            </ul>
+        </div>
+        {% endif %}
+    </div>
+</body>
+</html>
+
+<script>
+    document.addEventListener("DOMContentLoaded", function() {
+        // 使用 flatpickr 初始化日期时间选择器,支持秒级别
+        flatpickr("#start_time", {
+            enableTime: true,        // 启用时间选择
+            enableSeconds: true,     // 在时间选择器中是否可以选择秒
+            noCalendar: false,       // 显示日历
+            dateFormat: "Y-m-d H:i:S", // 设置格式为 yyyy-mm-dd HH:MM:SS(包括秒)
+            time_24hr: true,         // 使用 24 小时制
+            minuteIncrement: 1,      // 分钟增量设置为 1
+            secondIncrement: 1       // 秒增量设置为 1(确保秒数可编辑)
+        });
+
+        flatpickr("#end_time", {
+            enableTime: true,        // 启用时间选择
+            enableSeconds: true,     // 在时间选择器中是否可以选择秒
+            noCalendar: false,       // 显示日历
+            dateFormat: "Y-m-d H:i:S", // 设置格式为 yyyy-mm-dd HH:MM:SS(包括秒)
+            time_24hr: true,         // 使用 24 小时制
+            minuteIncrement: 1,      // 分钟增量设置为 1
+            secondIncrement: 1       // 秒增量设置为 1(确保秒数可编辑)
+        });
+    });
+</script>