用 AI 过滤交易信号

我在基于规则的逻辑之上添加了一个小的 Gemini AI 过滤器。机器人计算设置,构建最近市场状况的快照,并询问 Gemini 是否环境看起来足够干净还是太混乱。

用 AI 过滤交易信号
一键发币: x402兼容 | Aptos | X Layer | SUI | SOL | BNB | ETH | BASE | ARB | OP | Polygon | Avalanche

我想要一个小型的机器人来帮我监控 EURUSD,遵循一套明确的规则,并且只在市场看起来良好的时候发送清晰的警报。我并不是试图预测未来或建立一个神奇的赚钱机器。我只是想要结构。这个想法很简单:让机器人做枯燥的部分,遵守规则,并避免过度交易。

大多数基础机器人一旦有任何指标交叉就会触发。这可能是一个单一的 EMA 交叉或一个简单的 RSI 水平。在实际交易中,你从不依赖于一个因素。你会看趋势、动量、波动性、蜡烛大小,甚至一天中的时间。我希望我的机器人更像一个耐心的交易者。它应该等待多个条件达成一致,然后才发送信号。

为了使它更有趣,我在基于规则的逻辑之上添加了一个小的 Gemini AI 过滤器。机器人计算设置,构建最近市场状况的快照,并询问 Gemini 是否环境看起来足够干净还是太混乱。Gemini 只能说 APPROVE 或 SKIP。如果它批准,信号就会通过。如果它跳过,机器人会忽略该设置并继续前进。

1、工作原理

机器人的流程图

这个机器人是用 Python 编写的,并在一个简单的循环中运行。它使用 yfinance 获取 EURUSD 蜡烛数据,使用 pandas 和 numpy 处理数据,使用 pytz 处理时区,使用 requests 与 Telegram 通信。如果存在 Gemini API 密钥,脚本还会导入 google.generativeai 库并启用 AI 过滤器。如果密钥缺失或未安装该库,机器人将不带 AI 层运行,并退回到纯基于规则的系统。

在脚本的顶部,我保存了所有主要的设置。机器人在 1 分钟的时间框架上监视 EURUSD 符号。我定义 EMA 快速和慢速周期为 9 和 21,RSI 周期为 14,MACD 快速、慢速和信号周期,以及布林带参数。我还设置了检查间隔为 60 秒,因此主循环每分钟运行一次。

另一个重要的参数是 ONE_SIGNAL_PER_HOUR。当此标志打开时,机器人将在每个小时开始时尝试创建新信号。这意味着它仍然每分钟唤醒一次,但除非当前分钟是零,否则会忽略详细的信号逻辑。这保持了警报的稀有性和易于跟踪。

2、将机器人连接到 Telegram

我希望机器人感觉像一个通知流,而不是一个复杂的终端程序。因此,所有输出都发送到 Telegram 聊天。在脚本中,一个小的帮助函数构建对 Telegram Bot API 的请求,发送消息,并打印响应状态。

每当机器人启动时,它会发送一条简短的消息,说明高级 EURUSD 信号机器人已上线,并且仅用于演示和教育用途。稍后,当出现新信号或交易结果准备就绪时,机器人会发送一条清晰的消息,包含方向、价格、时间和信号原因。每天结束时,它还会发送一个简短的每日总结,包括盈利和亏损。

3、从 Yahoo Finance 获取 EURUSD 数据

每次机器人想检查信号时,它首先从 Yahoo Finance 获取新鲜的 EURUSD 数据。它下载两天的一分钟蜡烛,以便所有指标都有足够的历史数据进行计算。然后函数清理数据并确保有一个适当的 Close 列。Yahoo Finance 有时会返回多级列,所以代码处理这种情况并选择一个有效的 Close 系列。

如果数据缺失、为空或不包含可用的 Close 列,机器人会打印错误并跳过该周期。如果数据看起来良好,它将继续并添加指标。

4、添加 EMA、RSI、MACD 和布林带

一旦清洁的价格数据准备好,机器人会添加技术指标。首先,它在 Close 价格上计算 EMA 9 和 EMA 21。这两条线显示短期方向并帮助检测交叉。

接下来,它使用基于指数移动平均的平滑收益和损失公式计算 RSI。这会给出一个介于 0 和 100 之间的值,显示市场目前是上涨还是下跌,以及是否进入更极端的区域。

然后,机器人通过从快速 EMA 中减去慢速 EMA 来计算 MACD,构建信号线,并获取 MACD 直方图作为它们之间的差异。直方图显示动量有多强以及方向如何。

最后,它通过取移动平均线并加上或减去标准差的倍数来计算布林带。机器人存储中间带、上带和下带。这使得策略能够看到价格是否被压在近期范围的顶部或底部,并避免在极端波动中买入。

所有指标添加完成后,脚本删除具有缺失值的行,因此每根剩余的蜡烛都有完整的特征集。

5、交易时间和每日总结

机器人使用迪拜时间作为其主要时钟。它只在周一至周五的 11:00 至 22:00 迪拜时间之间交易。如果当前时间在此窗口之外或如果是周末,机器人只是打印一条消息并不会寻找新的交易。

该脚本还跟踪每日表现。它存储日期、胜利次数和失败次数。每次循环运行时,它都会检查迪拜的日期是否发生变化。如果新的一天开始了,机器人会向 Telegram 发送前一天的每日总结,然后重置新一天的计数器。

6、使用五分钟趋势过滤器

这个机器人的一项主要改进是更高时间框架的趋势过滤器。机器人将一分钟的收盘价重新采样为五分钟的蜡烛,然后对重新采样的系列应用 20 个周期的 EMA。如果最后五分钟的收盘价高于这个 EMA,趋势被认为是上升的。如果低于,则认为是下降的。如果没有足够的五分钟数据来计算稳定的 EMA,该函数返回 nothing 并且机器人跳过该次交易。

信号逻辑尊重这个趋势过滤器。只有在五分钟趋势向上时,机器人才允许发出 CALL 信号;只有在五分钟趋势向下时,机器人才允许发出 PUT 信号。这有助于避免与市场主要方向相反的交易。

7、蜡烛实体过滤器和时间过滤器

在机器人决定信号之前,它还会检查当前蜡烛实体的大小。如果 Open 列可用,它会测量 Open 和 Close 的绝对差,并将其与最近二十个蜡烛的平均实体大小进行比较。如果当前实体相对于最近的平均值太小,机器人会将其视为能量不足或犹豫不决的蜡烛并跳过。

时间过滤器随后检查交易窗口。如果不在我之前定义的迪拜时间范围内,机器人会忽略任何潜在的信号。这确保了所有交易符合特定的日常安排,而不是全天候运行。

8、信号逻辑的工作方式

当时间合适且数据准备就绪时,机器人会查看 DataFrame 中的最后两根蜡烛。它读取当前和前一个 EMA 快速和慢速值,当前 RSI,当前 MACD 直方图,当前布林带上下带和当前价格。

对于可能的 CALL 设置,快速 EMA 必须在最新蜡烛上交叉高于慢速 EMA,而在前一根蜡烛上低于或等于。五分钟趋势必须向上。RSI 必须处于健康的牛市范围内,不要太低,也不要极度超买。MACD 直方图必须为正,这表示向上动量。最后,当前价格必须低于上布林带,这样机器人就不会在已经涨到范围顶部的尖峰中买入。

对于可能的 PUT 设置,条件是镜像的。快速 EMA 必须交叉低于慢速 EMA。五分钟趋势必须向下。RSI 必须位于合理的熊市范围内,不要太高的,也不要深度超卖。MACD 直方图必须为负,这表示向下动量。价格必须高于下布林带,这样机器人就不会在价格已经暴跌到底部后卖出。

如果这些完整条件集中的任何一个都没有满足,机器人会打印一条消息,说明这一根蜡烛没有信号,并且什么也不做。

9、Gemini AI 过滤器

当基于规则的信号出现并通过所有条件时,机器人会构建一个紧凑的快照给 Gemini。它会获取最后五十根一分钟蜡烛,并记录三个列表:最近的收盘价、最近的 RSI 值和最近的 MACD 直方图值。它还包括信号的方向、五分钟趋势、当前价格、当前 RSI、当前 MACD 直方图、当前布林带上下带、当前蜡烛实体大小、最近的平均实体大小和迪拜时间。

这个快照会被转换成一个提示,发送给 Gemini 模型。提示告诉 Gemini,这是一个用于基于规则的机器人的教学用的过滤器。它不允许提供财务建议、谈论真实资金或建议仓位大小。它只能做一个决定:APPROVE 或 SKIP。它必须在第一行输出这个决定,然后在第二行给出一个非常简短的解释,说明环境看起来是否干净且方向明确,或者混乱且杂乱无章。

如果 Gemini 被禁用或调用失败,函数默认返回 APPROVE。这样机器人就像一个正常的基于规则的系统一样运行。如果 Gemini 返回 SKIP,机器人会忽略信号并不开仓。如果 Gemini 返回 APPROVE,机器人会继续正常的入场逻辑并将信号发送到 Telegram。

10、跟踪交易和评估结果

每当信号最终被批准时,机器人会将交易存储在一个列表中。每个条目包含蜡烛时间、入场价格、方向(CALL 或 PUT)以及交易打开的迪拜时间。在 DataFrame 中,机器人定义了到期时间为一分钟后的一个固定数量的蜡烛。在设置中,这个值被设为 5,因此逻辑上的到期时间是在入场蜡烛后的五个一分钟后。

交易的评估发生在检查新信号的同一函数中。然而,由于 ONE_SIGNAL_PER_HOUR 是开启的,该函数只在迪拜当前分钟为零时运行其主要逻辑。这意味着实际上,机器人围绕每个小时的顶部对交易评估进行了分组。代码仍然使用 DataFrame 中的五个蜡烛距离来决定比较哪个价格,但实际的检查是在下一个小时周期中进行的,此时加载了新的数据。

当有足够的数据覆盖到期蜡烛时,机器人会将退出价格与入场价格进行比较。对于 CALL 交易,如果退出价格高于入场价则计为胜利。对于 PUT 交易,如果退出价格低于入场价则计为胜利。然后它会发送一条详细的信息到 Telegram,包含入场价格和时间、退出价格和时间以及结果。根据这个结果,它更新当天的胜利或失败计数器。

import os  
import time  
from datetime import datetime, timedelta, date  
import requests  
import numpy as np  
import pandas as pd  
import yfinance as yf  
import pytz  
import traceback  

# 尝试导入 Gemini 客户端  
try:  
    import google.generativeai as genai  
    GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")  
    if GEMINI_API_KEY:  
        genai.configure(api_key=GEMINI_API_KEY)  
        GEMINI_ENABLED = True  
        print("Gemini AI 过滤器已启用。")  
    else:  
        GEMINI_ENABLED = False  
        print("GEMINI_API_KEY 未设置。Gemini AI 过滤器已禁用。")  
except ImportError:  
    GEMINI_ENABLED = False  
    print("google-generativeai 未安装。Gemini AI 过滤器已禁用。")  

# ========== 配置 ==========  
PAIR = "EURUSD=X"            # 在 Yahoo Finance 上的 EURUSD  
INTERVAL = "1m"              # 1 分钟蜡烛  

EMA_FAST = 9  
EMA_SLOW = 21  

RSI_PERIOD = 14  
MACD_FAST = 12  
MACD_SLOW = 26  
MACD_SIGNAL = 9  

BB_PERIOD = 20  
BB_STD = 2  

CHECK_EVERY_SECONDS = 60     # 每分钟检查一次  

EXIT_CANDLES = 5             # 到期需要多少个 1m 蜡烛 (5 分钟)  
ONE_SIGNAL_PER_HOUR = True   # 如果为 True,则只在每小时开始时允许信号  

# ======== Telegram 设置 ========  
TELEGRAM_BOT_TOKEN = "yours"  
TELEGRAM_CHAT_ID = "yourchatid"  
# =============================  

dubai_tz = pytz.timezone("Asia/Dubai")  

# 全局状态  
last_candle_time = None  
open_trades = []  # 字典列表: {entry_time, entry_price, direction, dubai_time_open}  

# 每日统计  
stats_date = None      # 类型: date  
wins_today = 0  
losses_today = 0  

# ========== Telegram 辅助程序 ==========  
def send_telegram_message(text: str):  
    """  
    向您的 Telegram 聊天发送纯文本消息。  
    """  
    url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"  
    payload = {  
        "chat_id": TELEGRAM_CHAT_ID,  
        "text": text  
    }  
    try:  
        r = requests.post(url, json=payload, timeout=10)  
        print("Telegram 状态:", r.status_code, "-", r.text)  
        if r.status_code != 200:  
            print("发送 Telegram 消息出错:", r.text)  
    except Exception as e:  
        print("Telegram 发送错误:", e)  

# ========== 数据获取 ==========  
def fetch_data() -> pd.DataFrame:  
    """  
    从 Yahoo Finance 获取最近的 EURUSD 蜡烛。  
    标准化列以确保始终有一个可用的 'Close' 列。  
    """  
    try:  
        df = yf.download(  
            PAIR,  
            period="2d",  
            interval=INTERVAL,  
            auto_adjust=False,  
            progress=False  
        )  
    except Exception as e:  
        print("从 Yahoo Finance 获取数据出错:", e)  
        return pd.DataFrame()  

    if df is None:  
        print("Yahoo Finance 返回 None")  
        return pd.DataFrame()  

    if isinstance(df, pd.Series):  
        df = df.to_frame("Close")  

    if not isinstance(df, pd.DataFrame):  
        print("从 yfinance 得到意外的数据类型:", type(df))  
        return pd.DataFrame()  

    if df.empty:  
        print("Yahoo Finance 返回空 DataFrame")  
        return pd.DataFrame()  

    # 如果列是 MultiIndex,选择一个 Close 列  
    if isinstance(df.columns, pd.MultiIndex):  
        if ("Close", PAIR) in df.columns:  
            df = df.copy()  
            df["Close"] = df[("Close", PAIR)]  
        else:  
            close_cols = [c for c in df.columns if c[0] == "Close"]  
            if close_cols:  
                df = df.copy()  
                df["Close"] = df[close_cols[0]]  
            else:  
                print("无法在 MultiIndex 数据中找到 Close 列")  
                return pd.DataFrame()  
    else:  
        if "Close" not in df.columns:  
            print("数据中没有 Close 列")  
            return pd.DataFrame()  

    df = df.dropna()  
    return df  

# ========== 指标 ==========  
def compute_rsi(close: pd.Series, period: int) -> pd.Series:  
    delta = close.diff()  
    gain = delta.clip(lower=0)  
    loss = -delta.clip(upper=0)  

    avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean()  
    avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean()  

    rs = avg_gain / (avg_loss + 1e-10)  
    rsi = 100 - (100 / (1 + rs))  
    return rsi  

def compute_macd(close: pd.Series, fast: int, slow: int, signal: int):  
    ema_fast = close.ewm(span=fast, adjust=False).mean()  
    ema_slow = close.ewm(span=slow, adjust=False).mean()  
    macd = ema_fast - ema_slow  
    signal_line = macd.ewm(span=signal, adjust=False).mean()  
    hist = macd - signal_line  
    return macd, signal_line, hist  

def compute_bollinger(close: pd.Series, period: int, std_mult: float):  
    ma = close.rolling(period).mean()  
    std = close.rolling(period).std()  
    upper = ma + std_mult * std  
    lower = ma - std_mult * std  
    return ma, upper, lower  

def add_indicators(df: pd.DataFrame) -> pd.DataFrame:  
    """  
    在 DataFrame 中添加 EMA 9、EMA 21、RSI、MACD、布林带。  
    """  
    if df is None or df.empty:  
        return pd.DataFrame()  

    df = df.copy()  

    close_raw = df["Close"]  

    # 如果 Close 是 DataFrame (罕见情况),取第一列  
    if isinstance(close_raw, pd.DataFrame):  
        close_col = close_raw.iloc[:, 0]  
    else:  
        close_col = close_raw  

    close = pd.to_numeric(close_col, errors="coerce")  
    close = pd.Series(close, index=df.index)  

    # EMAs  
    ema_fast = close.ewm(span=EMA_FAST, adjust=False).mean()  
    ema_slow = close.ewm(span=EMA_SLOW, adjust=False).mean()  
    df["ema_fast"] = ema_fast  
    df["ema_slow"] = ema_slow  

    # RSI  
    df["rsi"] = compute_rsi(close, RSI_PERIOD)  

    # MACD  
    macd, macd_signal, macd_hist = compute_macd(close, MACD_FAST, MACD_SLOW, MACD_SIGNAL)  
    df["macd"] = macd  
    df["macd_signal"] = macd_signal  
    df["macd_hist"] = macd_hist  

    # 布林带  
    bb_ma, bb_upper, bb_lower = compute_bollinger(close, BB_PERIOD, BB_STD)  
    df["bb_ma"] = bb_ma  
    df["bb_upper"] = bb_upper  
    df["bb_lower"] = bb_lower  

    # 删除具有任何 NaN 的行  
    df = df.dropna()  
    return df  

# ========== 每日统计 ==========  
def check_and_reset_daily_stats():  
    """  
    如果迪拜日期改变,发送前一天的摘要并重置计数器。  
    """  
    global stats_date, wins_today, losses_today  

    now_dubai = datetime.now(dubai_tz)  
    today = now_dubai.date()  

    if stats_date is None:  
        # 第一次初始化  
        stats_date = today  
        wins_today = 0  
        losses_today = 0  
        return  

    if today != stats_date:  
        # 新的一天开始,发送前一天的摘要  
        summary = (  
            f"📊 前一天的每日摘要 {stats_date.isoformat()} (迪拜时间)\n"  
            f"胜利: {wins_today}\n"  
            f"失败: {losses_today}"  
        )  
        print(summary)  
        send_telegram_message(summary)  

        # 重置新一天  
        stats_date = today  
        wins_today = 0  
        losses_today = 0  

# ========== 时间过滤器 ==========  
def is_good_trading_time(now_dubai: datetime) -> bool:  
    """  
    仅在主要外汇时间交易:  
    周一至周五,迪拜时间 11:00 至 22:00。  
    """  
    weekday = now_dubai.weekday()  # 0 = 星期一,6 = 星期日  
    if weekday >= 5:  
        return False  
    hour = now_dubai.hour  
    return 11 <= hour < 22  

# ========== 五分钟趋势 ==========  
def get_5m_trend(df: pd.DataFrame) -> str:  
    """  
    使用重新采样的五分钟蜡烛上的 EMA 确定五分钟趋势。  
    返回 "UP"、"DOWN" 或 None。  
    """  
    if df is None or df.empty:  
        return None  

    # 使用 Close 进行五分钟重新采样  
    close_1m = df["Close"]  
    if isinstance(close_1m, pd.DataFrame):  
        close_1m = close_1m.iloc[:, 0]  
    close_1m = pd.to_numeric(close_1m, errors="coerce")  

    df_5m = close_1m.resample("5T").last().dropna()  
    if len(df_5m) < 30:  
        return None  

    ema_5m = df_5m.ewm(span=20, adjust=False).mean()  
    last_close = df_5m.iloc[-1]  
    last_ema = ema_5m.iloc[-1]  

    if last_close > last_ema:  
        return "UP"  
    elif last_close < last_ema:  
        return "DOWN"  
    else:  
        return None  

# ========== 交易评估 ==========  
def evaluate_open_trades(df: pd.DataFrame):  
    """  
    检查是否有任何开放交易已完成 EXIT_CANDLES 蜡烛,  
    然后发送 WIN 或 LOSS 结果并更新每日统计。  
    """  
    global open_trades, wins_today, losses_today  

    if df is None or df.empty:  
        return  

    if len(open_trades) == 0:  
        return  

    index_list = list(df.index)  
    trades_to_remove = []  

    for trade in open_trades:  
        entry_time = trade["entry_time"]  
        direction = trade["direction"]  
        entry_price = float(trade["entry_price"])  

        if entry_time not in df.index:  
            continue  

        try:  
            entry_idx = index_list.index(entry_time)  
        except ValueError:  
            continue  

        exit_idx = entry_idx + EXIT_CANDLES  # 在 1 分钟图表上到期  

        if exit_idx >= len(df):  
            continue  

        exit_candle = df.iloc[exit_idx]  
        exit_price = float(exit_candle["Close"])  

        # 决定赢或输  
        if direction == "CALL":  
            result = "WIN" if exit_price > entry_price else "LOSS"  
        else:  # PUT  
            result = "WIN" if exit_price < entry_price else "LOSS"  

        dubai_time_open = trade["dubai_time_open"]  
        dubai_time_close = dubai_time_open + timedelta(minutes=EXIT_CANDLES)  

        msg = (  
            f"关于 {direction} EURUSD 交易的结果\n"  
            f"入场价格: {entry_price:.5f} 在 {dubai_time_open.strftime('%Y-%m-%d %H:%M:%S')} 迪拜时间\n"  
            f"在 {EXIT_CANDLES} 分钟后退出: {exit_price:.5f} 在 {dubai_time_close.strftime('%Y-%m-%d %H:%M:%S')} 迪拜时间\n"  
            f"结果: {result}"  
        )  
        print(msg)  
        send_telegram_message(msg)  

        # 更新统计  
        if result == "WIN":  
            wins_today += 1  
        else:  
            losses_today += 1  

        trades_to_remove.append(trade)  

    for t in trades_to_remove:  
        if t in open_trades:  
            open_trades.remove(t)  

# ========== Gemini AI 过滤器辅助程序 ==========  

def build_ai_snapshot(df: pd.DataFrame,  
                      direction: str,  
                      trend_5m: str,  
                      rsi_now: float,  
                      macd_hist_now: float,  
                      price_now: float,  
                      bb_upper_now: float,  
                      bb_lower_now: float,  
                      body_size: float,  
                      avg_body: float,  
                      now_dubai: datetime) -> dict:  
    """  
    为 AI 过滤器构建最近市场状况的紧凑快照。  
    我们保持它小巧,以便提示轻量。  
    """  
    recent = df.tail(50).copy()  

    # 限制为浮点数,保留合理的精度以避免巨大的字符串  
    closes = [float(x) for x in recent["Close"].round(5).tolist()]  
    rsis = [float(x) for x in recent["rsi"].round(2).tolist()]  
    macd_hists = [float(x) for x in recent["macd_hist"].round(6).tolist()]  

    snapshot = {  
        "direction": direction,  
        "trend_5m": trend_5m,  
        "price_now": float(price_now),  
        "rsi_now": float(rsi_now),  
        "macd_hist_now": float(macd_hist_now),  
        "bb_upper_now": float(bb_upper_now),  
        "bb_lower_now": float(bb_lower_now),  
        "body_size": float(body_size),  
        "avg_body": float(avg_body),  
        "recent_closes": closes,  
        "recent_rsi": rsis,  
        "recent_macd_hist": macd_hists,  
        "time_dubai": now_dubai.strftime('%Y-%m-%d %H:%M:%S'),  
    }  
    return snapshot  

def ask_gemini_should_trade(snapshot: dict) -> tuple[str, str]:  
    """  
    询问 Gemini 是否应 APPROVE 或 SKIP 此信号。  

    返回:  
        (decision, explanation)  
        - decision: "APPROVE" 或 "SKIP"  
        - explanation: 简短的人类可读原因  

    如果 Gemini 被禁用或出错,我们默认返回 APPROVE,这样机器人像以前一样运行。  
    这仅用于教育/演示用途,不是财务建议。  
    """  
    if not GEMINI_ENABLED:  
        return "APPROVE", "Gemini 过滤器已禁用。"  

    prompt = f"""  
你是一个教育交易助手,帮助过滤来自基于规则的 EURUSD 机器人的信号。  
你**不允许**提供财务建议、利润保证或仓位规模指导。  
你只决定这个信号看起来是否足够干净可以考虑,或者应该被跳过。  

数据(最近的 EURUSD 条件):  

方向: {snapshot['direction']}  # CALL 表示买入,PUT 表示卖出  
五分钟趋势方向(来自 EMA): {snapshot['trend_5m']}  
当前价格: {snapshot['price_now']}  
当前 RSI (1m): {snapshot['rsi_now']}  
当前 MACD 直方图 (1m): {snapshot['macd_hist_now']}  
当前布林带上限: {snapshot['bb_upper_now']}  
当前布林带下限: {snapshot['bb_lower_now']}  
当前蜡烛实体大小: {snapshot['body_size']}  (最近 20 个实体的平均值: {snapshot['avg_body']})  
本地时间 (迪拜): {snapshot['time_dubai']}  

最近的收盘价 (1m,最近的最后,最多 50 个值):  
{snapshot['recent_closes']}  

最近的 RSI 值 (1m):  
{snapshot['recent_rsi']}  

最近的 MACD 直方图值 (1m):  
{snapshot['recent_macd_hist']}  

你的任务:  
1. 决定这个信号看起来是否处于相对干净、方向明确的环境,还是混乱、不稳定或范围内的环境。  
2. 保持保守。如果你不确定,优先跳过。  
3. 输出正好两行:  
   - 第一行: 无论是 APPROVE 还是 SKIP (大写)。  
   - 第二行: 一个简短的句子解释你的决定,从教育的角度。  

记住:  
- 不要提及真实资金。  
- 不要告诉用户进行交易、调整仓位或承诺好的结果。  
- 你的答案只是一个教育过滤器,叠加在现有的基于规则的机器人之上。  
"""  

    try:  
        model = genai.GenerativeModel("gemini-1.5-flash")  
        response = model.generate_content(prompt)  
        text = (response.text or "").strip()  
        if not text:  
            return "APPROVE", "AI 返回空响应,默认为 APPROVE。"  

        lines = [line.strip() for line in text.splitlines() if line.strip()]  
        decision = lines[0].upper() if lines else "APPROVE"  
        if decision not in ("APPROVE", "SKIP"):  
            decision = "APPROVE"  
        explanation = lines[1] if len(lines) > 1 else "AI 没有解释。"  
        return decision, explanation  
    except Exception as e:  
        print("Gemini 在 ask_gemini_should_trade 中出错:", repr(e))  
        return "APPROVE", "AI 过滤器出错,默认为 APPROVE。"  

# ========== 信号逻辑 ==========  
def format_dubai_time_short(dt: datetime) -> str:  
    """  
    格式化时间如 2:55pm (没有前导零,小写 am/pm)。  
    """  
    return dt.strftime("%I:%M%p").lstrip("0").lower()  

def check_for_signal():  
    """  
    每根蜡烛的逻辑:  
    - 检查每日重置  
    - 获取数据  
    - 添加指标  
    - 评估现有交易  
    - 检查新多过滤器信号 (CALL/PUT)  
    - 可选: 询问 Gemini AI 是否应 APPROVE 或 SKIP  
    - 每小时模式: 如果 ONE_SIGNAL_PER_HOUR 为 True,则只在每小时开始时触发信号  
    """  
    global last_candle_time, open_trades  

    # 1) 每日摘要检查  
    check_and_reset_daily_stats()  

    now_dubai = datetime.now(dubai_tz)  

    # 仅在每小时开始时允许信号,如果启用  
    if ONE_SIGNAL_PER_HOUR:  
        if now_dubai.minute != 0:  
            print(f"{now_dubai} - 不是每小时开始,跳过信号检查。")  
            return  

    # 2) 获取数据  
    df = fetch_data()  
    if df is None or df.empty:  
        print(f"{now_dubai} - 从 Yahoo Finance 未收到数据")  
        return  

    # 3) 指标  
    df = add_indicators(df)  
    if df is None or df.empty:  
        print(f"{now_dubai} - 指标后数据不足")  
        return  

    # 4) 评估之前的开放交易  
    evaluate_open_trades(df)  

    # 5) 需要至少 3 根蜡烛  
    if len(df) < 3:  
        print(f"{now_dubai} - 信号检查蜡烛不足")  
        return  

    latest = df.iloc[-1]  
    prev = df.iloc[-2]  

    candle_time = latest.name  
    price_now = float(latest["Close"])  

    ema_fast_now = float(latest["ema_fast"])  
    ema_slow_now = float(latest["ema_slow"])  
    ema_fast_prev = float(prev["ema_fast"])  
    ema_slow_prev = float(prev["ema_slow"])  

    rsi_now = float(latest["rsi"])  
    macd_hist_now = float(latest["macd_hist"])  
    bb_upper_now = float(latest["bb_upper"])  
    bb_lower_now = float(latest["bb_lower"])  

    # 蜡烛实体大小过滤器 (固定为浮点数)  
    if "Open" in df.columns:  
        open_now = float(latest["Open"])  
        body_series = (df["Close"] - df["Open"]).abs()  
        avg_body = float(body_series.tail(20).mean())  
    else:  
        open_now = price_now  
        avg_body = 0.0  

    body_size = abs(price_now - open_now)  

    # 如果 avg_body 是零或 NaN,回退到当前实体大小  
    if not np.isfinite(avg_body) or avg_body == 0:  
        avg_body = body_size  

    # 避免微小的十字蜡烛 (太弱)  
    if avg_body > 0 and body_size < 0.3 * avg_body:  
        print(f"{now_dubai} - 蜡烛太小 (实体 {body_size:.6f} vs 平均 {avg_body:.6f}), 跳过。")  
        return  

    # 避免在非交易时间交易  
    if not is_good_trading_time(now_dubai):  
        print(f"{now_dubai} - 在主要交易时间之外,跳过交易。")  
        return  

    # 6) 五分钟趋势过滤器  
    trend_5m = get_5m_trend(df)  
    if trend_5m is None:  
        print(f"{now_dubai} - 五分钟趋势不明确,跳过。")  
        return  

    # 避免在同一根蜡烛上重复信号  
    if last_candle_time is not None and candle_time <= last_candle_time:  
        print(f"{now_dubai} - 尚未有新蜡烛 (上次蜡烛时间 {last_candle_time})。")  
        return  

    signal_text = None  
    direction = None  
    reason_parts = []  

    # 基础 EMA 交叉条件  
    call_cross = (ema_fast_prev <= ema_slow_prev) and (ema_fast_now > ema_slow_now)  
    put_cross = (ema_fast_prev >= ema_slow_prev) and (ema_fast_now < ema_slow_now)  

    # === CALL 条件 ===  
    if call_cross:  
        # 趋势过滤器: 仅在上涨趋势中买入  
        if trend_5m != "UP":  
            print(f"{now_dubai} - CALL 交叉但五分钟趋势不是 UP,跳过。")  
        # RSI: 避免超买; 想要牛市但不极端  
        elif not (40 <= rsi_now <= 70):  
            print(f"{now_dubai} - CALL 交叉但 RSI={rsi_now:.2f} 在 40-70 之外,跳过。")  
        # MACD: 需要牛市动量  
        elif macd_hist_now <= 0:  
            print(f"{now_dubai} - CALL 交叉但 MACD 直方图 {macd_hist_now:.6f} <= 0,跳过。")  
        # 布林带: 避免在或高于上轨的尖峰  
        elif price_now >= bb_upper_now:  
            print(f"{now_dubai} - CALL 交叉但价格在或高于上布林带,跳过。")  
        else:  
            signal_text = "买入 (CALL)"  
            direction = "CALL"  
            reason_parts.append("EMA9 交叉高于 EMA21")  
            reason_parts.append("五分钟趋势 UP")  
            reason_parts.append(f"RSI={rsi_now:.1f} 在 40-70")  
            reason_parts.append("MACD 动量牛市")  
            reason_parts.append("价格低于上布林带")  

    # === PUT 条件 ===  
    if direction is None and put_cross:  
        # 趋势过滤器: 仅在下跌趋势中卖出  
        if trend_5m != "DOWN":  
            print(f"{now_dubai} - PUT 交叉但五分钟趋势不是 DOWN,跳过。")  
        # RSI: 避免超卖; 想要熊市但不极端  
        elif not (30 <= rsi_now <= 60):  
            print(f"{now_dubai} - PUT 交叉但 RSI={rsi_now:.2f} 在 30-60 之外,跳过。")  
        # MACD: 需要熊市动量  
        elif macd_hist_now >= 0:  
            print(f"{now_dubai} - PUT 交叉但 MACD 直方图 {macd_hist_now:.6f} >= 0,跳过。")  
        # 布林带: 避免在或低于下轨的尖峰  
        elif price_now <= bb_lower_now:  
            print(f"{now_dubai} - PUT 交叉但价格在或低于下布林带,跳过。")  
        else:  
            signal_text = "卖出 (PUT)"  
            direction = "PUT"  
            reason_parts.append("EMA9 交叉低于 EMA21")  
            reason_parts.append("五分钟趋势 DOWN")  
            reason_parts.append(f"RSI={rsi_now:.1f} 在 30-60")  
            reason_parts.append("MACD 动量熊市")  
            reason_parts.append("价格高于下布林带")  

    # ========== AI 过滤器 + 最终信号 ==========  
    if signal_text is not None and direction is not None:  
        # 构建 AI 快照  
        ai_snapshot = build_ai_snapshot(  
            df=df,  
            direction=direction,  
            trend_5m=trend_5m,  
            rsi_now=rsi_now,  
            macd_hist_now=macd_hist_now,  
            price_now=price_now,  
            bb_upper_now=bb_upper_now,  
            bb_lower_now=bb_lower_now,  
            body_size=body_size,  
            avg_body=avg_body,  
            now_dubai=now_dubai  
        )  

        ai_decision, ai_reason = ask_gemini_should_trade(ai_snapshot)  

        if ai_decision != "APPROVE":  
            print(f"{now_dubai} - AI 过滤器跳过 {direction} 信号。原因: {ai_reason}")  
            # 如果你想在 Telegram 中看到 AI 跳过,请取消注释:  
            # send_telegram_message(f"AI 过滤器跳过了一个 {direction} 信号。原因: {ai_reason}")  
            return  

        # AI 批准,继续正常  
        last_candle_time = candle_time  

        dubai_entry_time = now_dubai  
        dubai_expiry_time = dubai_entry_time + timedelta(minutes=EXIT_CANDLES)  

        open_trades.append({  
            "entry_time": candle_time,  
            "entry_price": price_now,  
            "direction": direction,  
            "dubai_time_open": dubai_entry_time  
        })  

        first_line = (  
            f"{signal_text} EURUSD (每小时信号) 在 "  
            f"{format_dubai_time_short(dubai_entry_time)} 迪拜时间进行 {EXIT_CANDLES} 分钟"  
        )  

        reason_text = "; ".join(reason_parts)  

        msg = (  
            f"{first_line}\n\n"  
            f"入场价格: {price_now:.5f}\n"  
            f"入场时间: {dubai_entry_time.strftime('%Y-%m-%d %H:%M:%S')} 迪拜时间\n"  
            f"到期: {dubai_expiry_time.strftime('%Y-%m-%d %H:%M:%S')} 迪拜时间\n"  
            f"原因: {reason_text}\n\n"  
            f"AI 过滤器: {ai_decision} - {ai_reason}"  
        )  

        print(msg)  
        send_telegram_message(msg)  
    else:  
        print(f"{now_dubai} - 本根蜡烛没有信号。")  

# ========== 主循环 ==========  
def main_loop():  
    print("正在启动信号机器人 (带有可选 Gemini 过滤器的高级策略,每小时信号)...")  
    send_telegram_message(  
        "✅ 高级 EURUSD 信号机器人已启动 "  
        "(EMA+RSI+MACD+BB+5m 趋势 + 可选 Gemini 过滤器,每小时信号)。仅用于演示/教育用途。"  
    )  
    while True:  
        try:  
            check_for_signal()  
        except Exception as e:  
            print("主循环出错:", repr(e))  
            traceback.print_exc()  
        time.sleep(CHECK_EVERY_SECONDS)  

if __name__ == "__main__":  
    main_loop()

11、结束语

这个机器人并不是用来预测市场的或保证结果的。它只是一个简单的例子,展示了 Python、简单的指标、更高时间框架的趋势检查和轻量级 AI 过滤器如何协同工作。目标是减少噪音,保持一致性,并研究基于规则的逻辑在实时中的行为。如果你决定自己构建类似的东西,请保持简单,花时间,并把它当作一个学习项目。


原文链接:I Built an Advanced EURUSD Signal Bot With EMA, RSI, MACD, Bollinger Bands and a Gemini AI Filter

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。