From 0ae7f38f2096a6b8132eb3f1ade1be7f547b925f Mon Sep 17 00:00:00 2001 From: lzybetter Date: Sun, 5 Jul 2026 19:32:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E5=90=AB=E5=A4=9A=E5=9B=A0=E5=AD=90=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA=E9=87=8F=E5=8C=96=E7=AD=96=E7=95=A5=E5=8F=8A?= =?UTF-8?q?=20TG=20=E6=9C=BA=E5=99=A8=E4=BA=BA=E7=9B=91=E5=90=AC=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 46 ++++++++++ bot_listener.py | 172 +++++++++++++++++++++++++++++++++++++ config.sample.ini | 17 ++++ main.py | 178 +++++++++++++++++++++++++++++++++++++++ quant_strategy.py | 210 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 40 +++++++++ util.py | 89 ++++++++++++++++++++ 7 files changed, 752 insertions(+) create mode 100644 .gitignore create mode 100644 bot_listener.py create mode 100644 config.sample.ini create mode 100644 main.py create mode 100644 quant_strategy.py create mode 100644 requirements.txt create mode 100644 util.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70f8c02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# ========================================== +# 🚨 核心核心:严禁上传的个人隐私与本地配置 +# ========================================== +config.ini +*.conf +*.env +.env + +# ========================================== +# 📊 本地数据与状态文件(拒绝上传 SQLite 库和本地日志) +# ========================================== +*.db +*.db-journal +*.sqlite3 +*.log +quant_daily.log +bot_listener.log +*.csv +test.py + +# ========================================== +# 🐍 Python 环境与虚拟环境(只传代码,不传依赖包) +# ========================================== +.venv/ +venv/ +ENV/ +env/ +dist/ +build/ +*.egg-info/ +__pycache__/ +*.py[cod] +*$py.class + +# ========================================== +# 💻 操作系统与 IDE 特有缓存 +# ========================================== +.idea/ +.vscode/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.swp +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/bot_listener.py b/bot_listener.py new file mode 100644 index 0000000..d9954dd --- /dev/null +++ b/bot_listener.py @@ -0,0 +1,172 @@ +import os +import sys +import sqlite3 +import time +import logging +import configparser +import requests + +# ===================================================================== +# 0. 初始化基础环境与日志 +# ===================================================================== +current_dir = os.path.dirname(os.path.abspath(__file__)) +log_filename = os.path.join(current_dir, "bot_listener.log") +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(levelname)s] %(message)s', + handlers=[logging.FileHandler(log_filename, encoding='utf-8'), logging.StreamHandler(sys.stdout)] +) + +# 读取 config.ini +config = configparser.ConfigParser() +config_path = os.path.join(current_dir, "config.ini") +if not os.path.exists(config_path): + logging.error("❌ 错误:找不到 config.ini 配置文件!") + sys.exit(1) + +config.read(config_path, encoding='utf-8') +BOT_TOKEN = config.get("telegram", "bot_token") +CHAT_ID = int(config.get("telegram", "chat_id")) +DB_FILE = os.path.join(current_dir, config.get("database", "db_name", fallback="watchlist.db")) + +# 👇 核心核心:读入代理配置并构建成 requests 专用的字典 +PROXY_ENABLED = config.getint("proxy", "enabled", fallback=0) +PROXY_URL = config.get("proxy", "url", fallback="") + +PROXIES = None +if PROXY_ENABLED and PROXY_URL: + PROXIES = { + "http": PROXY_URL, + "https": PROXY_URL + } + logging.info(f"🚀 机器人监听服务已成功挂载代理: {PROXY_URL}") +else: + logging.warning("⚠️ 机器人监听服务当前未启用代理,国内环境可能会连接超时。") + + +# ===================================================================== +# 1. 数据库管理指令封装 +# ===================================================================== +def db_add_stock(code, name): + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + cursor.execute("INSERT OR REPLACE INTO watchlist (stock_code, stock_name, is_active) VALUES (?, ?, 1)", + (code, name)) + conn.commit() + conn.close() + return f"✅ 成功添加/激活股票:`{code}` ({name})" + + +def db_remove_stock(code): + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + cursor.execute("UPDATE watchlist SET is_active = 0 WHERE stock_code = ?", (code,)) + conn.commit() + conn.close() + return f"❌ 成功将股票 `{code}` 移出自选监控" + + +def db_list_watchlist(): + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + cursor.execute("SELECT stock_code, stock_name FROM watchlist WHERE is_active = 1") + rows = cursor.fetchall() + conn.close() + if not rows: + return "📋 当前量化关注列表为空。\n使用 `/add 代码 名称` 添加。" + msg = "📋 *当前量化系统关注列表*:\n" + for row in rows: + msg += f"🔹 `{row[0]}` | {row[1]}\n" + return msg + + +# ===================================================================== +# 2. 消息处理与分发 +# ===================================================================== +def send_reply(text): + """发送回执消息(带代理)""" + url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage" + try: + requests.post( + url, + json={"chat_id": CHAT_ID, "text": text, "parse_mode": "Markdown"}, + proxies=PROXIES, # 👈 带上代理 + timeout=10 + ) + except Exception as e: + logging.error(f"发送回执失败: {e}") + + +def handle_message(message): + from_id = message.get("from", {}).get("id") + # 安全风控 + if from_id != CHAT_ID: + logging.warning(f"拒绝了未授权用户 {from_id} 的指令") + return + + text = message.get("text", "").strip() + if not text.startswith("/"): + return + + parts = text.split() + cmd = parts[0].lower() + + if cmd == "/list": + reply = db_list_watchlist() + elif cmd == "/add": + if len(parts) < 3: + reply = "⚠️ 格式错误!请输入: `/add 代码 名称`\n例如: `/add 300750.SZ 宁德时代`" + else: + reply = db_add_stock(parts[1].upper(), parts[2]) + elif cmd == "/del": + if len(parts) < 2: + reply = "⚠️ 格式错误!请输入: `/del 代码`\n例如: `/del 300750.SZ`" + else: + reply = db_remove_stock(parts[1].upper()) + elif cmd in ["/start", "/help"]: + reply = "🤖 *量化做T小助手指令集*:\n\n" \ + "🔍 查看列表:/list\n" \ + "➕ 增加关注:`/add 代码 名称`\n" \ + "➖ 移除关注:`/del 代码`" + else: + reply = "❓ 未知指令,输入 /help 查看帮助。" + + send_reply(reply) + + +# ===================================================================== +# 3. 长轮询核心监听流 +# ===================================================================== +def main_loop(): + logging.info("Telegram 监听守护进程进入主循环...") + offset = 0 + + while True: + try: + url = f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates" + params = {"offset": offset, "timeout": 20} + + # 👇 核心核心:获取消息的 get 请求也必须带上代理 + response = requests.get( + url, + params=params, + proxies=PROXIES, + timeout=25 + ).json() + + if response.get("ok") and response.get("result"): + for update in response["result"]: + offset = update["update_id"] + 1 + if "message" in update: + handle_message(update["message"]) + + except requests.exceptions.RequestException as e: + logging.error(f"网络连接异常(可能代理挂了),5秒后重试: {e}") + time.sleep(5) + except Exception as e: + logging.error(f"监听进程捕获未知错误: {e}", exc_info=True) + time.sleep(2) + + +if __name__ == "__main__": + main_loop() \ No newline at end of file diff --git a/config.sample.ini b/config.sample.ini new file mode 100644 index 0000000..6622d75 --- /dev/null +++ b/config.sample.ini @@ -0,0 +1,17 @@ +[telegram] +# 你的 Telegram Bot Token (找 @BotFather 申请得到) +bot_token = +# 你的个人 Chat ID 或频道/群组 ID +chat_id = + +[database] +# SQLite 数据库文件名 +db_name = watchlist.db + + +[proxy] +# 🚨 开启代理开关:1 为启用,0 为关闭 +enabled = 0 +# 🚨 填入你 Linux 本地或局域网的代理软件端口(支持 http 或 socks5) +# 比如你本地运行了 Clash/V2Ray,通常 http 端口是 7890 或 10809 +url = http://127.0.0.1:7890 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..d602c1b --- /dev/null +++ b/main.py @@ -0,0 +1,178 @@ +import os +import sys +import sqlite3 +import logging +import configparser +from datetime import datetime, timedelta +import pandas as pd +import numpy as np +import requests + +from quant_strategy import QuantStateMachine +from util import get_history_k, get_5min_k +# ===================================================================== +# 初始化基础环境与日志 +# ===================================================================== +current_dir = os.path.dirname(os.path.abspath(__file__)) +log_filename = os.path.join(current_dir, "quant_daily.log") +logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', + handlers=[logging.FileHandler(log_filename, encoding='utf-8')]) + +config = configparser.ConfigParser() +config.read(os.path.join(current_dir, "config.ini"), encoding='utf-8') +BOT_TOKEN = config.get("telegram", "bot_token") +CHAT_ID = config.get("telegram", "chat_id") +DB_FILE = os.path.join(current_dir, config.get("database", "db_name", fallback="watchlist.db")) + +PROXY_ENABLED = config.getint("proxy", "enabled", fallback=0) +PROXY_URL = config.get("proxy", "url", fallback="") + +# ===================================================================== +# 补全缺失的函数 1: 真实/模拟数据接口 (请在此替换为你实际的 adata 调用代码) +# ===================================================================== +def fetch_data_from_adata(stock_code): + # 🚨 注意:这里是模拟数据,实际请使用你的真实 adata 接口替换 + end_date = datetime.today().strftime('%Y%m%d') + start_date = (datetime.today() - timedelta(days=100)).strftime('%Y%m%d') + daily_df = get_history_k(str(stock_code), start_date=start_date, end_date=end_date) + min_df = get_5min_k(stock_code, start_date='20260703', end_date='20260703') + return daily_df, min_df + + +# ===================================================================== +# 补全缺失的类 2: 针对 Telegram 消息推送优化的子类状态机 +# ===================================================================== +class TelegramQuantStateMachine(QuantStateMachine): + def generate_telegram_report(self, daily_df, min_df=None): + from quant_strategy import FeatureEngine, StateRuleBook + engine = FeatureEngine(daily_df, min_df) + f = engine.calculate_all_features() + + next_state, comment = StateRuleBook.evaluate_next_state(self.current_state, f) + self.current_state = next_state + + msg = f"📊 *【量化做T复盘报告】* \n" + msg += f"🤖 股票代码: `{self.stock_code}`\n" + msg += f"🕒 诊断时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n" + msg += f"📈 因子: `%B`={f['percent_b']:.2f} | 分时偏离={f['min_bias']:.2%} | 带宽分位={f['bw_quantile']:.2%}\n" + msg += f"🔍 状态: *{self.current_state}*\n" + msg += f"📝 诊断: _{comment}_\n" + msg += "-" * 30 + "\n" + msg += f"💡 *[明日手动操作指南]*:\n" + + if self.current_state in ["STATE_1_OSCILLATION", "STATE_1_OSCILLATION_SQUEEZE"]: + if self.current_state == "STATE_1_OSCILLATION_SQUEEZE": + msg += "⚠️ *[变盘警告]*:弹簧已压紧,随时大突破,做T手速要快!\n" + if f['percent_b'] >= 1.0 or f['min_bias'] > 0.035: + msg += f"🟢 *【建议高抛】*:当前处于震荡高位(价格:{f['close']}),建议尾盘或明日开盘*手动卖出网格仓*!" + elif f['percent_b'] <= 0.0 or f['min_bias'] < -0.035: + msg += f"🔴 *【建议低吸】*:当前处于震荡超跌区(价格:{f['close']}),建议尾盘或明日开盘*手动买回筹码*!" + else: + msg += "⚪ *【建议观望】*:处于安全中枢内,未触及边界,明天*不要乱动*。" + elif self.current_state == "STATE_3_MAIN_WAVE": + msg += "🔥 *【强烈建议死守】*:科技股主升浪狂飙中!*禁止日内做T高抛*,锁仓死拿,享受主升浪最大利润!" + elif self.current_state == "STATE_4_WAVE_END": + msg += "⚠️ *【建议减仓】*:主升浪确认破位结束。建议手动*大举高抛/清空做T仓位*,落袋为安。" + elif self.current_state == "STATE_2_DOWN_BREAK": + msg += "🛑 *【严禁抄底】*:技术形态向下破位崩塌!明天*千万不要低吸接飞刀*,保持观望。" + elif self.current_state == "STATE_5_BOTTOMING": + msg += "🌱 *【建议潜伏】*:个股地量筑底阶段。不建议激进日内做T,但适合长线资金手动分批*定投埋伏*。" + + return msg + + +# ===================================================================== +# 补全缺失的函数 3: 数据库状态加载与固化 +# ===================================================================== +def load_saved_state(stock_code): + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + cursor.execute("SELECT current_state FROM stock_states WHERE stock_code = ?", (stock_code,)) + row = cursor.fetchone() + conn.close() + return row[0] if row else "STATE_1_OSCILLATION" + + +def save_current_state(stock_code, state): + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + cursor.execute("INSERT OR REPLACE INTO stock_states (stock_code, current_state, update_time) VALUES (?, ?, ?)", + (stock_code, state, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) + conn.commit() + conn.close() + + +# ===================================================================== +# 主运行入口 +# ===================================================================== +def main(): + logging.info("量化定时任务触发...") + try: + conn = sqlite3.connect(DB_FILE) + df = pd.read_sql_query("SELECT stock_code FROM watchlist WHERE is_active = 1", conn) + conn.close() + watchlist = df['stock_code'].tolist() + except Exception as e: + logging.error(f"读取数据库自选列表失败: {e}") + return + + if not watchlist: + logging.warning("当前没有激活的关注股票。") + return + + for stock_code in watchlist: + try: + daily_df, min_df = fetch_data_from_adata(stock_code) + if daily_df.empty: + logging.warning(f"[{stock_code}] 数据为空,跳过") + continue + + saved_state = load_saved_state(stock_code) + machine = TelegramQuantStateMachine(initial_state=saved_state) + machine.stock_code = stock_code + tg_report = machine.generate_telegram_report(daily_df, min_df) + + # 推送大字报到 TG + url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage" + proxies = None + if PROXY_ENABLED and PROXY_URL: + proxies = { + "http": PROXY_URL, + "https": PROXY_URL + } + + # 将 proxies 字典作为参数传入 requests + response = requests.post( + url, + json={"chat_id": CHAT_ID, "text": tg_report, "parse_mode": "Markdown"}, + proxies=proxies, + timeout=60 + ) + save_current_state(stock_code, machine.current_state) + except Exception as e: + logging.error(f"[{stock_code}] 运行时异常: {e}", exc_info=True) + + +def init_database_safely(): + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS watchlist ( + stock_code TEXT PRIMARY KEY, + stock_name TEXT, + is_active INTEGER DEFAULT 1 + ) + """) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS stock_states ( + stock_code TEXT PRIMARY KEY, + current_state TEXT, + update_time TEXT + ) + """) + conn.commit() + conn.close() + +if __name__ == "__main__": + init_database_safely() # 👈 核心:让 main.py 每次运行时也自己检查并建表 + main() \ No newline at end of file diff --git a/quant_strategy.py b/quant_strategy.py new file mode 100644 index 0000000..b905c79 --- /dev/null +++ b/quant_strategy.py @@ -0,0 +1,210 @@ +import numpy as np +import pandas as pd +import talib + + +# ===================================================================== +# 1. 指标引擎:未来所有新发掘的指标,全写在这里 +# ===================================================================== +class FeatureEngine: + + def __init__(self, daily_df, min5_df): + """ + daily_df: 日线数据 DataFrame + min5_df: 当天 5分钟 级别 K 线数据 DataFrame (需要包含 'close', 'volume') + """ + self.df = daily_df.copy() + self.min5_df = min5_df.copy() + self._extract_basic_arrays() + + def _extract_basic_arrays(self): + self.close = self.df["close"].astype(float).values + self.high = self.df["high"].astype(float).values + self.low = self.df["low"].astype(float).values + self.amount = self.df["amount"].astype(float).values + + def calculate_all_features(self): + features = {} + + # ===================================================================== + # 1. 🔄 【核心修改】:用 5分钟 K 线精确估算今日分时均价 + # ===================================================================== + if not self.min5_df.empty: + # 计算每 5 分钟的成交金额(收盘价 * 成交量) + # 注意:如果你的 5分钟接口直接自带 'amount'(成交额) 列,请直接用 self.min5_df['amount'] + m5_close = self.min5_df["close"].astype(float) + m5_volume = self.min5_df["volume"].astype(float) + + total_amount = (m5_close * m5_volume).sum() + total_volume = m5_volume.sum() + + if total_volume > 0: + # 算出截止到当前(14:48)的 A 股全天分时均价 + vwap_today = total_amount / total_volume + # 当前最新价格(最后一根 5分钟线的收盘价,最接近实时现价) + current_price = m5_close.iloc[-1] + + # 计算偏离度:(最新价 - 分时均价) / 分时均价 + features["min_bias"] = (current_price - vwap_today) / vwap_today + else: + features["min_bias"] = 0.0 + else: + features["min_bias"] = 0.0 + + # ===================================================================== + # 2. 日线常规指标计算(保持你的经典布林带逻辑不变) + # ===================================================================== + up, mid, low = talib.BBANDS( + self.close, timeperiod=20, nbdevup=2, nbdevdn=2, matype=0 + ) + self.df["bb_up"], self.df["bb_mid"], self.df["bb_low"] = up, mid, low + + self.df["percent_b"] = (self.df["close"] - low) / (up - low) + self.df["bandwidth"] = (up - low) / mid + self.df["amount_avg_20d"] = ( + self.df["amount"].rolling(20).mean().shift(1) + ) + self.df["price_max_60d"] = self.df["close"].rolling(60).max().shift(1) + self.df["low_min_10d"] = self.df["low"].rolling(10).min().shift(1) + + latest = self.df.iloc[-1] + prev = self.df.iloc[-2] + + # 如果 5分钟线数据拿到了,现价以 5分钟最新收盘价为准(更接近 14:48 真实盘面) + # 如果没拿到,退化使用日线昨日收盘(做测试用) + features["close"] = ( + current_price if not self.min5_df.empty else latest["close"] + ) + + features["prev_close"] = prev["close"] + features["amount"] = latest["amount"] + features["amount_avg_20d"] = latest["amount_avg_20d"] + features["ma5"] = latest["ma5"] if "ma5" in latest else latest["close"] + features["ma10"] = latest["ma10"] if "ma10" in latest else latest["close"] # 👈 核心:补上这一行! + features["ma20"] = latest["bb_mid"] + features["ma60"] = latest["ma60"] if "ma60" in latest else latest["close"] # 👈 顺便把ma60也安全带上 + features["percent_b"] = latest["percent_b"] + features["bandwidth"] = latest["bandwidth"] + features["is_price_60d_max"] = ( + latest["close"] >= latest["price_max_60d"] + ) + features["is_not_new_low_10d"] = latest["close"] > latest["low_min_10d"] + + history_bw = self.df["bandwidth"].iloc[-250:] + features["bw_quantile"] = (history_bw < latest["bandwidth"]).mean() + + return features + +# ===================================================================== +# 2. 状态规则书:这里只根据指标数据定义状态切换门槛 +# ===================================================================== +class StateRuleBook: + + @staticmethod + def evaluate_next_state(current_state, f): + """f 传入的是 FeatureEngine 计算出来的最新特征字典""" + + # 计算主升浪基础多头条件 + is_ma_bull = (f["ma5"] > f["ma10"] > f["ma20"]) and ( + f["ma5"] > f["prev_ma5"] + ) + + # 核心决策流:利用解耦后的字典 f 进行条件拆解 + # 【判断是否从主升浪跌破】 + if ( + current_state == "STATE_3_MAIN_WAVE" + and f["close"] < f["ma10"] + and f["prev_close"] > f["prev_ma10"] + ): + return ( + "STATE_4_WAVE_END", + "⚠️ 主升浪确认结束!清空做T仓,准备重新激活做T。", + ) + + # 【判断是否爆发主升浪】 + if ( + is_ma_bull + and f["is_price_60d_max"] + and (f["amount"] > f["amount_avg_20d"] * 1.8) + ): + return ( + "STATE_3_MAIN_WAVE", + "🚀 主升浪开启 / 放量向上突破!做T脚本自动拉闸休眠,锁仓死拿!", + ) + + # 【判断是否向下破位】 + if f["percent_b"] < 0.0: # 跌破布林下轨 + return "STATE_2_DOWN_BREAK", "❌ 向下破位!停止低吸做T,转为观望。" + + # 【判断是否低量筑底】 + is_low_volume = f["amount"] < (f["amount_avg_20d"] * 0.5) + is_ma_converge = abs(f["ma5"] - f["ma20"]) / f["ma20"] < 0.03 + if f["is_not_new_low_10d"] and is_low_volume and is_ma_converge: + return ( + "STATE_5_BOTTOMING", + "🌱 下跌结束,正在地量筑底。允许开始尝试轻仓做T。", + ) + + # 【判断是否处于变盘前夜】 + if f["bw_quantile"] < 0.12: + return ( + "STATE_1_OSCILLATION_SQUEEZE", + "🎚️ 变盘前夜:带宽极度压缩。保持做T,但防范单边突破。", + ) + + # 默认返回横盘震荡 + return ( + "STATE_1_OSCILLATION", + "☕ 正常横盘震荡期。激活做T脚本,正常执行高抛低吸。", + ) + + +# ===================================================================== +# 3. 策略状态机:调度核心 +# ===================================================================== +class QuantStateMachine: + + def __init__(self, initial_state="STATE_1_OSCILLATION"): + self.current_state = initial_state + + def run_daily_diagnostic(self, daily_df, min_df=None): + # 1. 扔给传感器计算指标 + engine = FeatureEngine(daily_df, min_df) + features = engine.calculate_all_features() + + # 2. 扔给规则书判定状态 + next_state, comment = StateRuleBook.evaluate_next_state( + self.current_state, features + ) + + # 3. 更新并固化状态 + self.current_state = next_state + + # 4. 根据最终状态,分发当天的交易指令 + self._execute_trading_action(features, comment) + + def _execute_trading_action(self, f, comment): + print(f"\n[当前系统状态]: {self.current_state}") + print(f"[状态诊断提示]: {comment}") + + # 具体的交易动作分发 + if self.current_state in [ + "STATE_1_OSCILLATION", + "STATE_1_OSCILLATION_SQUEEZE", + ]: + # 只有在震荡期,才读取 %B 或分时执行做T + if f["percent_b"] >= 1.0 or f["min_bias"] > 0.035: + print(">>> 💰 【执行动作】:尾盘高抛,卖出 20% 网格仓。") + elif f["percent_b"] <= 0.0 or f["min_bias"] < -0.035: + print(">>> 🛒 【执行动作】:尾盘低吸,接回 20% 网格仓。") + else: + print(">>> ☕ 【执行动作】:未触及极端做T边界,长线持股观望。") + + elif self.current_state == "STATE_3_MAIN_WAVE": + print(">>> 🔒 【执行动作】:主升浪锁仓护航中,禁止任何人乱动做T筹码。") + + elif self.current_state == "STATE_2_DOWN_BREAK": + print(">>> 🛑 【执行动作】:市场向下破位,做T有被套风险,禁止低吸!") + + elif self.current_state == "STATE_5_BOTTOMING": + print(">>> 🔬 【执行动作】:地量筑底阶段,允许小仓位底部分批低吸。") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4e45b7a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,40 @@ +adata==2.9.5 +akracer==0.0.14 +akshare==1.18.64 +beautifulsoup4==4.15.0 +build==1.5.0 +certifi==2026.6.17 +cffi==2.0.0 +charset-normalizer==3.4.7 +curl_cffi==0.15.0 +decorator==5.3.1 +et_xmlfile==2.0.0 +html5lib==1.1 +idna==3.18 +jsonpath==0.82.2 +lxml==6.1.1 +markdown-it-py==4.2.0 +mdurl==0.1.2 +numpy==2.2.6 +openpyxl==3.1.5 +packaging==26.2 +pandas==2.3.3 +py-mini-racer==0.6.0 +pycparser==3.0 +Pygments==2.20.0 +pyproject_hooks==1.2.0 +python-dateutil==2.9.0.post0 +pytz==2026.2 +requests==2.34.2 +rich==15.0.0 +six==1.17.0 +soupsieve==2.8.4 +TA-Lib==0.7.0 +tabulate==0.10.0 +tomli==2.4.1 +tqdm==4.68.3 +typing_extensions==4.16.0 +tzdata==2026.2 +urllib3==2.7.0 +webencodings==0.5.1 +xlrd==2.0.2 diff --git a/util.py b/util.py new file mode 100644 index 0000000..d12c520 --- /dev/null +++ b/util.py @@ -0,0 +1,89 @@ +import requests +import pandas as pd + +CN_CODE_TYPE = { + "600":'sh', + "601":'sh', + "603":'sh', + "605":'sh', + "000":'sz', + "300":'sz', + "688":'sh', + "002":'sz', + "50":"sh", + "51":"sh", + "52":"sh", + "16":"sz", + "15":"sz", + "18":"sz" + } + + + +def get_history_k(code, start_date='19700101', end_date=None): + + if '.' in code: + code = code.split('.')[0] + + stock_code = '' + + if code[0:2] in CN_CODE_TYPE: + stock_code = code + '.' + CN_CODE_TYPE[code[0:2]] + elif code[0:3] in CN_CODE_TYPE: + stock_code = code + '.' + CN_CODE_TYPE[code[0:3]] + print(stock_code) + if stock_code != '': + + url = "https://api.zhituapi.com/hs/history/%s/d/f?token=6E0E86BC-15AD-4275-8A95-B02D168D63C1&st=%s"%(stock_code, start_date) + + if end_date is not None: + url += "&end_date=%s"%(end_date) + + response = requests.get(url) + + data = response.json() + print(data) + df = pd.DataFrame(data) + + df['t'] = df['t'].apply(pd.to_datetime) + df.rename({'t': 'date', 'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 'v': 'volume', 'a': 'amount'}, + axis='columns', inplace=True) + df = df[['date', 'open', 'high', 'low', 'close', 'volume', 'amount']].set_index('date') + + else: + df = pd.DataFrame() + + + return df + + +def get_5min_k(code, start_date='19700101', end_date=None): + stock_code = '' + + if code[0:2] in CN_CODE_TYPE: + stock_code = code + '.' + CN_CODE_TYPE[code[0:2]] + elif code[0:3] in CN_CODE_TYPE: + stock_code = code + '.' + CN_CODE_TYPE[code[0:3]] + + if stock_code != '': + + url = "https://api.zhituapi.com/hs/history/%s/5/n?token=6E0E86BC-15AD-4275-8A95-B02D168D63C1&st=%s"%(stock_code, start_date) + + if end_date is not None: + url += "&end_date=%s"%(end_date) + + response = requests.get(url) + + data = response.json() + + df = pd.DataFrame(data) + df['t'] = df['t'].apply(pd.to_datetime) + df.rename({'t': 'date', 'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 'v': 'volume', 'a': 'amount'}, + axis='columns', inplace=True) + df = df[['date','close', 'volume']].set_index('date') + + else: + df = pd.DataFrame() + + + return df