Visual Search for Thematic Libraries in Python Quantitative Analysis

Continuing from the previous article! I have created a visual search tool for the captured thematic data, reasons for price limits, and business descriptions, doubling the analysis efficiency!

1. Function Overview: Three-column Layout for Clear Information

Stock Search: Enter code/name to quickly locate the targetThematic Evidence Search: Enter keywords to find all stocks mentioned in business reviewsInformation Aggregation Display: Concept themes, reasons for price limits, and business reviews presented in one view

Final interface effect (three-column layout):

  • Left Column: Search results list (stocks + thematic evidence)
  • Middle Column: Business review search (supports multi-keyword AND queries)
  • Right Column: Detailed information display (including highlighted matches)

2. Core Implementation: Perfect Combination of Streamlit and Playwright

1. Data Loading and Mapping

def load_stock_mapping():
    """Load stock code and name mapping"""
    # Read stock list from Excel and create code-name dictionary
    df = pd.read_excel(EXCEL_FILE, usecols=[0, 1], skiprows=1, 
                       header=None, dtype=str)
    for code, name in zip(df[0], df[1]):
        if str(code).isdigit() and len(str(code)) == 6:
            stock_mapping[str(code)] = str(name)

2. Intelligent Search Logic (Exact + Fuzzy Dual Mode)

def search_stocks(keyword: str) -> List[Dict]:
    """Search stocks: prioritize exact matches, then fuzzy search"""
    results = []
    # Exact match (code or name)
    for stock in stock_list:
        if keyword in stock['name'] or keyword.zfill(6) == stock['code']:
            results.append(stock)
    
    # Fuzzy search (name pinyin, partial matches, etc.)
    if not results:
        for stock in stock_list:
            if keyword.lower() in stock['name'].lower():
                results.append(stock)
    return results[:50]  # Limit return quantity

3. Business Review Keyword Search (Multi-keyword AND Query)

def search_keyword_in_reports(keyword: str) -> List[Tuple[str, str, str]]:
    """Search for files containing all keywords in business reviews"""
    keywords = [kw.strip() for kw in keyword.split() if kw.strip()]
    
    for filename in os.listdir(REPORT_DIR):
        if filename.endswith('.txt'):
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Core: return only if all keywords match
            if all(kw in content for kw in keywords):
                results.append((stock_code, stock_name, filename))

4. Context Highlight Extraction (Optimizing Reading Experience)

def extract_context_from_content(content: str, keyword: str, 
                                 context_length: int = 500) -> List[str]:
    """Extract 500 characters of context before and after the keyword, and highlight"""
    pos = content.find(keyword, start)
    context_start = max(0, pos - context_length)
    context_end = min(len(content), pos + len(keyword) + context_length)
    
    # Highlight processing
    highlighted = context.replace(keyword, f'**{keyword}**')
    return [highlighted]

3. Step-by-step Deployment Tutorial

Environment Installation (2 minutes)

pip install streamlit pandas openpyxl
# openpyxl is used to read the Excel stock list

File Structure Preparation

D:\1jiuniu\F10ths\
├── gntc_ths\          # Concept themes (results from previous article)
├── ztyy_jygs\         # Reasons for price limits (see historical articles)
├── output_ths\        # Business reviews (see historical articles)
├── 全部A股.xlsx       # Stock code table
└── search_app.py      # Code for this tool

Running Method

streamlit run search_app.py

The browser will automatically open, and you can use it!

4. Practical Application Scenarios

Scenario 1: Thematic Linkage Mining

Search “Huawei Ascend”, find all stocks mentioned in business reviews → Check concept themes for verification → Quickly establish a target pool

Scenario 2: Price Limit Reason Tracing

Click on a stock → Right column displays “Reasons for Price Limit” → Combine with “Concept Themes” → Determine sustainability

Scenario 3: Fundamental Cross-validation

Search “Significant Performance Increase” → Filter stocks mentioning this term → Compare F10 thematic points → Identify true growth vs. pure speculation

Effect Diagram

Visual Search for Thematic Libraries in Python Quantitative Analysis
Visual Search for Thematic Libraries in Python Quantitative Analysis

However, the default seems a bit low-end, so we are using the latest Gemini 3.0 to help us create a more professional front-end display. The final effect diagram is as follows.

Visual Search for Thematic Libraries in Python Quantitative Analysis

5. Compliance Statement and Extension Suggestions

Disclaimer

  1. This tool is designed for personal research and learning and should not be used for commercial purposes
  2. Data scraping should comply with website service terms and control request frequency
  3. Investment carries risks; please refer to official announcements for data accuracy

6. Complete Code (Core Segments)

import streamlit as st
import pandas as pd
import os
import re
from typing import Dict, List, Tuple

# ---------------------------------------------------------
# Configuration and Style Settings
# ---------------------------------------------------------
st.set_page_config(
    page_title="Intelligent Investment Research · Thematic Mining Terminal",
    page_icon="📈",
    layout="wide",
    initial_sidebar_state="expanded"
)

# Custom CSS: Create a financial terminal style
st.markdown("""
<style>
    /* Global font and background */
    .main {
        background-color: #f5f7f9;
    }
    h1, h2, h3 {
        font-family: 'Microsoft YaHei', sans-serif;
        color: #1f2d3d;
    }
    
    /* Top title bar */
    .title-container {
        background: linear-gradient(90deg, #2c3e50 0%, #4ca1af 100%);
        padding: 1.5rem;
        border-radius: 8px;
        color: white;
        margin-bottom: 20px;
        box-shadow: 0 4px 6px rgba(0,0,0,0.1);
    }
    
    /* Search box style */
    .stTextInput > div > div > input {
        border: 2px solid #e0e0e0;
        border-radius: 8px;
        padding: 10px;
        font-size: 16px;
    }
    .stTextInput > div > div > input:focus {
        border-color: #4ca1af;
        box-shadow: 0 0 5px rgba(76, 161, 175, 0.3);
    }

    /* Sidebar result list button */
    .result-card {
        background-color: white;
        padding: 10px;
        border-radius: 6px;
        border-left: 4px solid #ddd;
        margin-bottom: 8px;
        transition: all 0.2s;
        cursor: pointer;
        box-shadow: 0 1px 3px rgba(0,0,0,0.05);
    }
    .result-card:hover {
        border-left-color: #4ca1af;
        transform: translateX(2px);
        box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    }
    
    /* Detail page card */
    .info-card {
        background-color: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0,0,0,0.05);
        margin-bottom: 15px;
        border-top: 3px solid #3498db;
    }
    
    /* Keyword highlight */
    .highlight-tag {
        background-color: #fff3cd;
        color: #856404;
        padding: 2px 6px;
        border-radius: 4px;
        font-weight: bold;
        border: 1px solid #ffeeba;
    }
    
    /* Tab style optimization */
    .stTabs [data-baseweb="tab-list"] {
        gap: 2px;
    }
    .stTabs [data-baseweb="tab"] {
        height: 50px;
        white-space: pre-wrap;
        background-color: #eef2f6;
        border-radius: 4px 4px 0 0;
        gap: 1px;
        padding-top: 10px;
        padding-bottom: 10px;
    }
    .stTabs [aria-selected="true"] {
        background-color: white;
        border-top: 2px solid #4ca1af;
        color: #4ca1af;
        font-weight: bold;
    }
</style>
""", unsafe_allow_html=True)

# ---------------------------------------------------------
# Data Path Configuration (Keep Original)
# ---------------------------------------------------------
GNTC_DIR = r"D:\1jiuniu\F10ths\gntc_ths"      # Concept theme directory
ZT_DIR = r"D:\1jiuniu\F10ths\ztyy_jygs"         # Price limit reason directory
EXCEL_FILE = r"D:\1jiuniu\全部A股.xlsx"        # Stock information file
REPORT_DIR = r"D:\1jiuniu\F10ths\output_ths"# Business review directory

# ---------------------------------------------------------
# Data Loading Logic (Add Cache Optimization)
# ---------------------------------------------------------

@st.cache_data
def load_stock_data():
    """Load stock code and name mapping (with cache)"""
    mapping = {}
    s_list = []
    try:
        if os.path.exists(EXCEL_FILE):
            df = pd.read_excel(EXCEL_FILE, usecols=[0, 1], skiprows=1, header=None, dtype=str)
            df = df.map(lambda x: x.strip() if isinstance(x, str) else x)
            codes = df[0].tolist()
            names = df[1].tolist()
            
            for c, n in zip(codes, names):
                if pd.notna(c) and pd.notna(n):
                    if str(c).isdigit() and len(str(c)) == 6:
                        mapping[str(c)] = str(n)
            
            s_list = [{'code': code, 'name': name} for code, name in mapping.items()]
            return mapping, s_list
    except Exception as e:
        st.error(f"Loading stock mapping failed: {e}")
        return {}, []

# Initialize global data
stock_mapping, stock_list = load_stock_data()

def load_gntc_content(code: str) -> str:
    """Load concept theme content"""
    try:
        file_path = os.path.join(GNTC_DIR, f"{code}.txt")
        if os.path.exists(file_path):
            with open(file_path, 'r', encoding='utf-8') as f:
                return f.read()
        return""# Return empty string if not found, interface processing
    except Exception:
        return""

def load_zt_content(code: str, name: str) -> str:
    """Load price limit reason content"""
    try:
        if not os.path.exists(ZT_DIR): return""
        
        # First try code matching
        for filename in os.listdir(ZT_DIR):
            if filename.endswith('.txt') and filename.startswith(code):
                with open(os.path.join(ZT_DIR, filename), 'r', encoding='utf-8') as f:
                    return f.read()
        
        # Then try name matching
        for filename in os.listdir(ZT_DIR):
            if filename.endswith('.txt') and name in filename:
                with open(os.path.join(ZT_DIR, filename), 'r', encoding='utf-8') as f:
                    return f.read()
        return""
    except Exception:
        return""

def search_logic(keyword: str) -> Tuple[List[Dict], List[Tuple[str, str, str]]]:
    """Core search logic"""
    if not keyword:
        return [], []
    
    # 1. Search stocks (code or name)
    s_results = []
    # Exact match
    for stock in stock_list:
        if keyword == stock['name'] or keyword == stock['code']:
            s_results.append(stock)
    
    # Fuzzy search (deduplication)
    seen_codes = {s['code'] for s in s_results}
    if len(s_results) < 50:
        for stock in stock_list:
            if stock['code'] in seen_codes: continue
            if keyword.lower() in stock['name'].lower() or keyword in stock['code']:
                s_results.append(stock)
                if len(s_results) >= 50: break
    
    # 2. Search business reviews (keyword evidence)
    r_results = []
    if os.path.exists(REPORT_DIR) and keyword.strip():
        keywords = [kw.strip() for kw in keyword.split() if kw.strip()]
        if keywords:
            temp_results = []
            # Traverse files (if there are too many files, consider optimizing the index later, currently keeping the original logic)
            # For performance, only traverse the most recently modified or limit the number, here keep the full amount but note performance
            try:
                files = [f for f in os.listdir(REPORT_DIR) if f.endswith('.txt')]
                # Simple optimization: only look at files containing all keywords in their content
                for filename in files:
                    file_path = os.path.join(REPORT_DIR, filename)
                    try:
                        with open(file_path, 'r', encoding='utf-8') as f:
                            content = f.read()
                        if all(kw in content for kw in keywords):
                            stock_code = filename.split('_')[0]
                            stock_name = stock_mapping.get(stock_code, "Unknown")
                            temp_results.append((stock_code, stock_name, filename))
                    except: continue
            except: pass
            
            # Deduplication to get the latest
            unique = {}
            for code, name, fname in temp_results:
                if code not in unique or fname > unique[code][2]:
                    unique[code] = (code, name, fname)
            r_results = list(unique.values())

    return s_results, r_results

def highlight_text(text: str, keywords: List[str]) -> str:
    """Highlight text using HTML"""
    if not text or not keywords:
        return text
    
    # Escape HTML to prevent injection, but here we control the content ourselves, so no strict escaping to support simple line breaks
    # Simple handling of line breaks
    text = text.replace("\n", "<br>")
    
    for kw in keywords:
        if not kw.strip(): continue
        # Use regex to replace ignoring case
        pattern = re.compile(re.escape(kw), re.IGNORECASE)
        text = pattern.sub(lambda m: f'<span class="highlight-tag">{m.group(0)}</span>', text)
    return text

def get_report_context(code: str, filename: str, search_keywords: List[str]) -> str:
    """Get and format research report content"""
    try:
        path = os.path.join(REPORT_DIR, filename)
        if not os.path.exists(path): return"File does not exist"
        with open(path, 'r', encoding='utf-8') as f:
            content = f.read()
            
        if not search_keywords:
            return content.replace("\n", "\n\n") # Markdown line break
            
        # Extract summary logic
        contexts = []
        for kw in search_keywords:
            # Simple context extraction
            indices = [m.start() for m in re.finditer(re.escape(kw), content)]
            for idx in indices[:5]: # Limit the number of matches for each word
                start = max(0, idx - 200)
                end = min(len(content), idx + 200)
                snippet = content[start:end]
                contexts.append(f"...{snippet}...")
        
        if contexts:
            return"\n\n---\n\n".join(contexts)
        else:
            return content # If no keywords found (possibly code matching), display full text
            
    except Exception as e:
        return f"Read error: {e}"

# ---------------------------------------------------------
# Main Interface Logic
# ---------------------------------------------------------
def main():
    # Top title
    st.markdown("""
    <div class="title-container">
        <h2 style="margin:0; color:white;">📊 Intelligent Investment Research · Thematic Mining Terminal</h2>
        <p style="margin:5px 0 0 0; opacity:0.8;">One-click penetration of all market A-share announcements, themes, and reasons for price limits</p>
    </div>
    """, unsafe_allow_html=True)

    # Search bar
    col_search, col_info = st.columns([3, 1])
    with col_search:
        search_term = st.text_input("🔍 Enter code / abbreviation / theme / keyword (e.g.: 600519, computing power, low-altitude economy)", placeholder="Supports fuzzy search and multi-keyword combinations")
    with col_info:
        if stock_mapping:
            st.metric("Recorded stocks", f"{len(stock_mapping)} companies")

    if not search_term:
        st.info("👆 Please enter keywords above to start mining...")
        return

    # Execute search
    stock_results, report_results = search_logic(search_term)
    
    # Layout: left list, right detail
    left_col, right_col = st.columns([1, 2.5])

    # --- Left: Result List ---
    with left_col:
        st.markdown("### 🎯 Search Results")
        
        # Use Session State to manage the currently selected item
        if 'selected_item' not in st.session_state:
            st.session_state.selected_item = None
        
        # Result merge display
        if not stock_results and not report_results:
            st.warning("No matching results found")
        
        # 1. Stock matching list
        if stock_results:
            st.markdown(f"**Stock Matches ({len(stock_results)})**")
            with st.container(height=300): # Scrollable area
                for s in stock_results:
                    # Use custom styled button logic
                    label = f"📈 {s['code']} {s['name']}"
                    if st.button(label, key=f"btn_s_{s['code']}", use_container_width=True):
                        st.session_state.selected_item = {'type': 'stock', 'data': s}
        
        # 2. Research report/theme matching list
        if report_results:
            st.markdown(f"**Thematic Evidence ({len(report_results)})**")
            with st.container(height=300):
                for code, name, fname in report_results:
                    label = f"📑 {name} ({code})"
                    if st.button(label, key=f"btn_r_{code}_{fname}", use_container_width=True):
                        st.session_state.selected_item = {'type': 'report', 'data': (code, name, fname)}

    # --- Right: Detail Display ---
    with right_col:
        selection = st.session_state.selected_item
        
        if not selection:
            st.markdown("""
            <div class="info-card" style="text-align:center; color:#666; padding:50px;">
                <h3>👈 Please select a stock on the left to view details</h3>
                <p>Supports viewing concept themes, price limit revelations, and original business reviews</p>
            </div>
            """, unsafe_allow_html=True)
        else:
            # Extract basic information
            if selection['type'] == 'stock':
                s_data = selection['data']
                code, name = s_data['code'], s_data['name']
                filename = None # Default load latest
            else:
                code, name, filename = selection['data']
            
            # Header card
            st.markdown(f"""
            <div class="info-card" style="border-top-color: #e74c3c; display:flex; justify-content:space-between; align-items:center;">
                <div>
                    <h1 style="margin:0; font-size: 28px; color:#e74c3c;">{name}</h1>
                    <span style="background:#f0f0f0; padding:2px 8px; border-radius:4px; color:#666; font-family:monospace;">{code}</span>
                </div>
                <div style="text-align:right;">
                    <span style="font-size:12px; color:#999;">Data Source: Tonghuashun F10</span>
                </div>
            </div>
            """, unsafe_allow_html=True)

            # Get data
            gntc = load_gntc_content(code)
            zt_reason = load_zt_content(code, name)
            
            # If coming from a keyword search, need to pass keywords for highlighting
            display_keywords = []
            if search_term and not search_term.isdigit():
                display_keywords = search_term.split()

            # Tab layout
            tab1, tab2, tab3 = st.tabs(["🧩 Concept Themes", "🚀 Price Limit Revelation", "📄 Business Review (Evidence)"])

            with tab1:
                if gntc:
                    st.markdown(f'<div class="info-card">{gntc.replace(chr(10), "<br>")}</div>', unsafe_allow_html=True)
                else:
                    st.info("No concept theme data available")

            with tab2:
                if zt_reason:
                    st.markdown(f'<div class="info-card">{zt_reason.replace(chr(10), "<br>")}</div>', unsafe_allow_html=True)
                else:
                    st.info("No recent price limit reason data available")

            with tab3:
                # Load research report/review
                # If filename is not specified (clicked from stock list), automatically find the latest
                current_file = filename
                if not current_file:
                     # Simple logic to find the latest file
                    all_files = [f for f in os.listdir(REPORT_DIR) if f.startswith(code)] if os.path.exists(REPORT_DIR) else []
                    if all_files:
                        all_files.sort(reverse=True) # Assume filenames contain dates and can be sorted
                        current_file = all_files[0]
                
                if current_file:
                    # Read original content
                    raw_content = ""
                    try:
                        with open(os.path.join(REPORT_DIR, current_file), 'r', encoding='utf-8') as f:
                            raw_content = f.read()
                    except:
                        raw_content = "File reading failed"

                    # Keyword highlighting processing
                    if display_keywords:
                        # Extract context and highlight
                        formatted_html = highlight_text(raw_content, display_keywords)
                        # If content is too long and there are matches, prioritize displaying matching paragraphs, but also provide an option to view the full text
                        st.markdown(f"""
                        <div class="info-card">
                            <div style="color:#666; font-size:0.9em; margin-bottom:10px;">Source file: {current_file}</div>
                            <div style="line-height:1.6; font-size:15px;">{formatted_html}</div>
                        </div>
                        """, unsafe_allow_html=True)
                    else:
                        # Display full text
                        st.text_area("File Content", raw_content, height=600)
                else:
                    st.warning(f"No business review file found for {name}")

if __name__ == "__main__":
    main()

Reasons for fluctuations and business descriptions reference:Python Quantitative Collection

Disclaimer: This tool is only an auxiliary tool for information acquisition, all data comes from third-party website pages. Users are responsible for the accuracy and completeness of the data, and any investment decision risks arising from the use of this tool are unrelated to the tool developers.

Leave a Comment