diff --git a/stock-html/db.py b/stock-html/db.py index 29a65e4..61b0073 100644 --- a/stock-html/db.py +++ b/stock-html/db.py @@ -552,7 +552,7 @@ def db_save_fundamental(code, data): # ========== 资金流向历史数据操作(数据库版) ========== -def db_get_fund_flow_history(code): +def db_get_fund_flow_history(code, limit=60): """获取股票的资金流向历史数据""" conn = get_db() if not conn: @@ -564,11 +564,14 @@ def db_get_fund_flow_history(code): SELECT code, trade_date::text, close_price, change_pct, main_net_inflow, main_net_inflow_pct, super_net_inflow, super_net_inflow_pct, - big_net_inflow, big_net_inflow_pct + big_net_inflow, big_net_inflow_pct, + mid_net_inflow, mid_net_inflow_pct, + small_net_inflow, small_net_inflow_pct FROM stock_fund_flow_history WHERE code = %s ORDER BY trade_date DESC - """, (code,)) + LIMIT %s + """, (code, limit)) rows = cur.fetchall() # 获取最新日期 diff --git a/stock-html/routes/analysis.py b/stock-html/routes/analysis.py index f21a6fa..df01d28 100644 --- a/stock-html/routes/analysis.py +++ b/stock-html/routes/analysis.py @@ -61,7 +61,7 @@ def analyze(): # 3. 获取实时价格补充到结果(优先腾讯API,兼容腾讯云) try: import requests as _req - _tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code + _tcode = ('sh' if stock_code.startswith('6') else 'bj' if stock_code.startswith(('8', '9')) else 'sz') + stock_code _r = _req.get(f'http://qt.gtimg.cn/q={_tcode}', timeout=5, headers={'Referer': 'https://finance.qq.com'}) if _r.status_code == 200 and '\"' in _r.text: diff --git a/stock-html/services/fund_flow_analyzer.py b/stock-html/services/fund_flow_analyzer.py index 23660cc..3e2466f 100644 --- a/stock-html/services/fund_flow_analyzer.py +++ b/stock-html/services/fund_flow_analyzer.py @@ -155,6 +155,39 @@ def _get_fund_flow_from_mairui(stock_code, days=10): 'small_net_inflow_pct': small_net_pct, }) + # 从本地DB补充 close_price 和 change_pct(避免全为0导致量价背离误判) + from db import get_db, put_db + conn = get_db() + if conn: + try: + cur = conn.cursor() + date_list = [r['date'] for r in records if r['date']] + if date_list: + cur.execute(""" + SELECT k.trade_date::text, k.close, + CASE WHEN prev.close > 0 + THEN ROUND((k.close - prev.close) / prev.close * 100, 2) + ELSE 0 END AS change_pct + FROM stock_kline_daily k + LEFT JOIN LATERAL ( + SELECT close FROM stock_kline_daily + WHERE code = k.code AND trade_date < k.trade_date + ORDER BY trade_date DESC LIMIT 1 + ) prev ON true + WHERE k.code = %s AND k.trade_date::text = ANY(%s) + """, (stock_code, date_list)) + price_map = {r[0]: {'close': float(r[1] or 0), 'change_pct': float(r[2] or 0)} + for r in cur.fetchall()} + for r in records: + info = price_map.get(r['date']) + if info: + r['close_price'] = info['close'] + r['change_pct'] = info['change_pct'] + except Exception as e: + logger.warning(f"补充K线价格失败({stock_code}): {e}") + finally: + put_db(conn) + # 按日期升序排列 records.sort(key=lambda x: x['date']) logger.info(f"麦蕊API获取{stock_code}资金流向{len(records)}条") @@ -245,17 +278,20 @@ def analyze_fund_flow(stock_code, days=5): # 出货:主力净流出但价格不跌(跌幅<2%) accumulation = False distribution = False - if total_main_inflow > 0: - price_changes = [r['change_pct'] for r in recent] - avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0 - if avg_price_change < 2: - accumulation = True + # 检查是否有有效的价格数据(close_price 全为0说明数据不完整,跳过背离检测) + has_valid_price = any(r.get('close_price', 0) > 0 for r in recent) + if has_valid_price: + if total_main_inflow > 0: + price_changes = [r['change_pct'] for r in recent] + avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0 + if avg_price_change < 2: + accumulation = True - if total_main_inflow < 0: - price_changes = [r['change_pct'] for r in recent] - avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0 - if avg_price_change > -2: - distribution = True + if total_main_inflow < 0: + price_changes = [r['change_pct'] for r in recent] + avg_price_change = sum(price_changes) / len(price_changes) if price_changes else 0 + if avg_price_change > -2: + distribution = True # 单日超大单突击 big_surge = False diff --git a/stock-html/services/mairui_api.py b/stock-html/services/mairui_api.py index a896d5a..6bbe7e3 100644 --- a/stock-html/services/mairui_api.py +++ b/stock-html/services/mairui_api.py @@ -245,17 +245,21 @@ def get_fund_flow(stock_code, days=3): flow_data = [] for item in data: # 计算主力净流入 = 主买大单+主买特大单 - 主卖大单-主卖特大单 - main_buy = float(item.get('zmbddcje', 0)) + float(item.get('zmbtdcje', 0)) - main_sell = float(item.get('zmsddcje', 0)) + float(item.get('zmstdcje', 0)) + main_buy = float(item.get('zmbddcje', 0) or 0) + float(item.get('zmbtdcje', 0) or 0) + main_sell = float(item.get('zmsddcje', 0) or 0) + float(item.get('zmstdcje', 0) or 0) main_net = main_buy - main_sell - + + # 日期解析:与 fund_flow_analyzer.py 保持一致,用字符串截取 + t_str = str(item.get('t', '')) + date_str = t_str[:10] if t_str else '' + flow_data.append({ - 'date': datetime.fromtimestamp(item.get('t', 0)).strftime('%Y-%m-%d') if item.get('t') else '', + 'date': date_str, 'main_net_inflow': main_net, - 'super_buy': float(item.get('zmbtdcje', 0)), - 'super_sell': float(item.get('zmstdcje', 0)), - 'big_buy': float(item.get('zmbddcje', 0)), - 'big_sell': float(item.get('zmsddcje', 0)), + 'super_buy': float(item.get('zmbtdcje', 0) or 0), + 'super_sell': float(item.get('zmstdcje', 0) or 0), + 'big_buy': float(item.get('zmbddcje', 0) or 0), + 'big_sell': float(item.get('zmsddcje', 0) or 0), }) return {'success': True, 'data': flow_data} diff --git a/stock-html/services/scheduler.py b/stock-html/services/scheduler.py index 382e4bb..01061aa 100644 --- a/stock-html/services/scheduler.py +++ b/stock-html/services/scheduler.py @@ -101,7 +101,7 @@ def is_trading_time(): def get_all_users(): """获取所有启用自动交易的用户""" - from db import get_db + from db import get_db, put_db from psycopg2.extras import RealDictCursor conn = get_db() @@ -121,7 +121,7 @@ def get_all_users(): print(f"[定时任务] 获取用户列表失败: {e}") return [] finally: - conn.close() + put_db(conn) # 自定义股票列表(100只精选股票) @@ -168,7 +168,7 @@ def execute_auto_trade_for_user(user_id, trade_quantity=1000, scan_date=None): scan_date: 使用哪天的扫描数据, None则自动选择最近可用的 """ - from db import get_db + from db import get_db, put_db from psycopg2.extras import RealDictCursor print(f"[定时任务] 开始为用户{user_id}执行策略交易(统一推荐算法)...") @@ -407,7 +407,7 @@ def execute_auto_trade_for_user(user_id, trade_quantity=1000, scan_date=None): traceback.print_exc() return {'error': str(e)} finally: - conn.close() + put_db(conn) def _get_latest_price(stock_code): @@ -417,7 +417,7 @@ def _get_latest_price(stock_code): def update_positions_price_for_user(user_id): """更新用户持仓的当前价格(收盘时调用)— 使用腾讯财经API""" - from db import get_db + from db import get_db, put_db from psycopg2.extras import RealDictCursor conn = get_db() @@ -443,6 +443,8 @@ def update_positions_price_for_user(user_id): for c in codes: if c.startswith('6'): tencent_codes.append(f'sh{c}') + elif c.startswith('8') or c.startswith('9'): + tencent_codes.append(f'bj{c}') else: tencent_codes.append(f'sz{c}') _r = _req.get(f'http://qt.gtimg.cn/q={",".join(tencent_codes)}', @@ -492,7 +494,7 @@ def update_positions_price_for_user(user_id): conn.rollback() print(f"[定时任务] 更新用户{user_id}持仓价格失败: {e}") finally: - conn.close() + put_db(conn) def job_morning_trade(): @@ -514,11 +516,11 @@ def job_morning_trade(): # 尝试使用智能引擎 try: from services.smart_trade_engine import execute_smart_trade - from db import get_db + from db import get_db, put_db conn = get_db() if conn: result = execute_smart_trade(conn, user_id, scan_date=None) - conn.close() + put_db(conn) if result.get('success'): print(f"[定时任务] 用户{user_id} 智能引擎执行成功 " f"(算法:{result.get('algo','?')}, 信号:{result.get('signals',0)})") @@ -553,11 +555,11 @@ def job_afternoon_trade(): # 尝试使用智能引擎 try: from services.smart_trade_engine import execute_smart_trade - from db import get_db + from db import get_db, put_db conn = get_db() if conn: result = execute_smart_trade(conn, user_id, scan_date=today) - conn.close() + put_db(conn) if result.get('success'): print(f"[定时任务] 用户{user_id} 午后智能引擎执行成功") continue @@ -645,7 +647,7 @@ def trigger_afternoon_trade(): def trigger_closing_update(): """手动触发收盘更新(仅更新持仓价格)""" - from db import get_db + from db import get_db, put_db if not is_trading_day(): print("[定时任务] 今天不是交易日,跳过") return diff --git a/stock-html/services/stock_algorithms.py b/stock-html/services/stock_algorithms.py index 7c78a99..939fb97 100644 --- a/stock-html/services/stock_algorithms.py +++ b/stock-html/services/stock_algorithms.py @@ -962,8 +962,8 @@ def _generate_plain_summary(price, change_pct, ma_trend, position, supports, parts.append(trend_desc + '。') # 2. 价格位置(用大白话) - pos_20 = position.get('20d', {}) - pct_20 = pos_20.get('pct', 50) + pos_20 = position.get('d20', {}) + pct_20 = pos_20.get('range_pct', 50) if pct_20 > 80: parts.append(f'当前股价处于近20天的高位区间({pct_20:.0f}%位置),已经涨了不少,追高要小心。') elif pct_20 > 50: diff --git a/stock-html/services/stock_service.py b/stock-html/services/stock_service.py index adf6a68..8bd309a 100644 --- a/stock-html/services/stock_service.py +++ b/stock-html/services/stock_service.py @@ -45,7 +45,7 @@ def get_stock_name(stock_code): # 腾讯财经API获取股票名称 try: import requests as _req - tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code + tcode = ('sh' if stock_code.startswith('6') else 'bj' if stock_code.startswith(('8', '9')) else 'sz') + stock_code _r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5, headers={'Referer': 'https://finance.qq.com'}) if _r.status_code == 200 and '\"' in _r.text: @@ -119,6 +119,8 @@ def get_stock_fund_flow(stock_code, start_date, end_date, force_refresh=False): market = 'sh' elif stock_code.startswith('0') or stock_code.startswith('3'): market = 'sz' + elif stock_code.startswith('8') or stock_code.startswith('9'): + market = 'bj' else: return None, None, "无法识别股票代码所属市场" @@ -313,7 +315,7 @@ def get_realtime_price(stock_code): # 备用方案2:使用腾讯财经API(腾讯云可用) try: import requests as _req - tcode = ('sh' if stock_code.startswith('6') else 'sz') + stock_code + tcode = ('sh' if stock_code.startswith('6') else 'bj' if stock_code.startswith(('8', '9')) else 'sz') + stock_code _r = _req.get(f'http://qt.gtimg.cn/q={tcode}', timeout=5, headers={'Referer': 'https://finance.qq.com'}) if _r.status_code == 200 and '\"' in _r.text: