Type/to search

TabFM谷歌预测框架策略

Common strategy
Created: 2026-07-09 13:03:02
Last modified: a month ago
2
Follow
508
Followers

TabFM 原始K线预测交易策略 — 描述手册

一、策略简介

本策略是运行在 FMZ 发明者量化平台上的 Python 单文件示范,核心思路是:不计算传统技术指标,而是把最近一段原始 OHLCV K线直接展开成"表格特征",交给 TabFM(表格基础模型 / Tabular Foundation Model) 预测下一根K线的方向(up / down / flat),再按置信度阈值执行交易。

⚠️ 重要声明:这是科普/教学示范,不是可直接实盘盈利的策略。默认 AUTO_TRADE=False,只打印信号不真实下单。若托管者环境未安装 tabfm,会自动降级为 mock 预测。


二、核心工作流程

启动 → 环境检查(numpy/pandas/tabfm) → 立即预测一次 ↓ 主循环 (每 CHECK_INTERVAL_S 秒): ├─ 处理交互命令 (清空状态 / 手动平仓) ├─ 每个自然小时整点触发一次 TabFM 预测 ├─ 持仓监控 (硬止损 + 移动止盈, 每轮都执行) └─ 刷新状态面板

运行频率设计

  • 进程启动后立即执行一次预测(trigger="startup"
  • 之后仅在每个自然小时整点触发(trigger="hourly"
  • 持仓的止损/移动止盈按 CHECK_INTERVAL_S 持续监控,不受小时节奏限制

三、特征工程原理(策略的灵魂)

3.1 样本构造 (build_dataset)

过去 raw_window已完成K线,预测紧接着的下一根K线方向。构造 train_rows 条训练样本 + 1 条待预测样本。

  • 样本排列:rows[0] 最新,rows[-1] 最旧
  • 行内排列:lag_1 距目标最近,lag_window 最远
  • 每根K线展开为 5 个特征:open / high / low / close / volume

3.2 归一化 (make_raw_kline_row)

关键防泄漏设计:

  • 价格类特征以目标K线前一根收盘价为锚点做相对偏移:(price - anchor) / anchor
  • 成交量除以窗口内平均成交量做标准化
  • 待预测样本 target_idx=len(bars) 只读取已知K线,无未来数据泄漏

3.3 标签 (make_next_bar_label)

根据目标K线相对前一根的收益率分类:

  • return > thresholdup
  • return < -thresholddown
  • 其余 → flat

说明:TabFM 把窗口视为固定列的表格模型,不是 TimesFM 那种原生时序模型。


四、预测与降级机制

模式触发条件说明
real_tabfm装了 tabfm 且 USE_REAL_TABFM=trueJAX 后端预训练模型,fit + predict_proba
mock_fallback真实预测抛异常降级为 mock
mock环境缺包或关闭真实模型基于近期收益 + K线压力的 sigmoid 打分

模型采用 MODEL_CACHE 全局缓存,避免重复加载。JAX 已强制 CPU 运行并关闭内存预占(文件头部环境变量)。


五、交易与风控逻辑

5.1 开仓 (open_position)

  • 头寸大小 = 风险现金 / 止损距离,风险现金 = 权益 × RISK_PCT
  • 止损距离 = max(ATR × HARD_STOP_ATR, last × 0.1%)
  • 名义价值上限 = 权益 × MAX_LEVERAGE

5.2 持仓监控 (monitor_position)

  • 硬止损:入场价 ± ATR × HARD_STOP_ATR
  • 移动止盈:盈利达 TRAIL_START_ATR 后启动,允许从最优价回撤 TRAIL_GIVEBACK_ATR 个 ATR
  • 最终止损取硬止损与移动止损的更优者

5.3 换仓与冷却

  • REVERSE_ON_SIGNAL=true:反向信号先平旧仓再开新仓
  • COOLDOWN_BARS:两次信号间的最小间隔(防频繁交易)
  • MIN_CONFIDENCE:置信度不足则 hold

六、参数一览

参数默认作用
SYMBOL交易对,留空用当前
PERIOD_NAMEPERIOD_M15K线周期
BAR_LIMIT260获取K线条数
RETURN_THRESHOLD0.003涨跌分类阈值
MIN_CONFIDENCE0.60最小交易置信度
USE_REAL_TABFMtrue是否用真实 TabFM
AUTO_TRADEfalse是否真实下单
RISK_PCT0.01单笔风险占权益比
MAX_LEVERAGE2名义价值上限倍数
HARD_STOP_ATR2.0硬止损 ATR 倍数
TRAIL_START_ATR2.0移动止损启动 ATR
TRAIL_GIVEBACK_ATR1.0移动止损回撤 ATR
REVERSE_ON_SIGNALtrue反向信号换仓
CHECK_INTERVAL_S10主循环轮询秒数
COOLDOWN_BARS3信号冷却周期

⚠️ 参数不一致提醒:代码内部读取 RAW_WINDOW / TRAIN_ROWSload_config 中),但 [Strategy Parameters] 里并未定义这两项,会一直走默认值 15 / 100。同时参数里的 LOOKAHEAD_BARS 在代码中未被使用(实际是预测"下一根",非前瞻多根)。建议对齐参数与代码。


七、交互命令

  • clear:store — 清空本地持久化状态
  • close:position — 手动平掉当前持仓

八、状态面板

通过 LogStatus 展示表格:模式、调度信息、最近预测时间、下次预测时间、up/flat/down 概率、置信度、持仓移动止损状态等。

Source
Python
# -*- coding: utf-8 -*-
"""
FMZ 发明者量化:TabFM 原始K线预测交易单文件示范

核心目标:
1. 这是发明者 FMZ Python 策略代码,可以直接复制到 FMZ 策略编辑器。
2. 启动时检查 Python 包,缺少则降级为 mock(不自动安装)。
3. 直接用 exchange.GetRecords() 获取 K 线。
4. 不计算技术指标,直接把最近一段 OHLCV K线展开为表格特征。
5. 用 TabFM 预测未来方向 up / down / flat 的概率。
6. 按概率阈值执行交易。

重要说明:
- 默认 AUTO_TRADE=False,只打印信号,不真实下单。
- 如果 FMZ 托管者环境未安装 tabfm,则自动降级为 mock 预测。
- 这是科普示范,不是可直接实盘的盈利策略。

建议 FMZ 参数:
SYMBOL: 字符串,例如 BTC_USDT.swap,可留空使用当前交易对
PERIOD_NAME: 字符串,例如 PERIOD_M15
BAR_LIMIT: 数字,默认 260
TRAIN_ROWS: 数字,默认 100,作为 TabFM 上下文的历史样本行数
RETURN_THRESHOLD: 数字,默认 0.0003,下一根K线涨跌分类阈值
MIN_CONFIDENCE: 数字,默认 0.60
USE_REAL_TABFM: 布尔,默认 true
AUTO_TRADE: 布尔,默认 false
RISK_PCT: 数字,默认 0.01
MAX_LEVERAGE: 数字,默认 2
HARD_STOP_ATR: 数字,默认 2.0
TRAIL_START_ATR: 数字,默认 2.0,盈利超过多少 ATR 后启动移动止损
TRAIL_GIVEBACK_ATR: 数字,默认 1.0,启动后允许回撤多少 ATR
REVERSE_ON_SIGNAL: 布尔,默认 true,反向信号出现时先平仓再开仓
CHECK_INTERVAL_S: 数字,默认 10
COOLDOWN_BARS: 数字,默认 3

运行频率:
- 策略每次启动后立即执行一次 TabFM 预测。
- 此后只在每个自然小时整点执行一次预测。
- 持仓止损与移动止盈仍按 CHECK_INTERVAL_S 持续监控。
"""

import os

# 必须在导入 JAX / TabFM 之前设置,避免 JAX 预占内存。
os.environ.setdefault("JAX_PLATFORMS", "cpu")
os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false")
os.environ.setdefault("XLA_PYTHON_CLIENT_ALLOCATOR", "platform")

import importlib
import json
import math
import time


STORE_KEY = "fmz_tabfm_onefile_strategy_v1"
STRATEGY_NAME = "fmz_tabfm_raw_kline_predictor"
MODEL_CACHE = {
    "model": None,
    "classifier": None,
    "available": None,
    "backend": "jax",
}

RAW_WINDOW_DEFAULT = 15
TRAIN_ROWS_DEFAULT = 100


def make_feature_columns(window):
    columns = []
    for lag in range(1, window + 1):
        prefix = "lag_{}".format(lag)
        columns.extend([
            prefix + "_open",
            prefix + "_high",
            prefix + "_low",
            prefix + "_close",
            prefix + "_volume",
        ])
    return columns


FEATURE_COLUMNS = make_feature_columns(RAW_WINDOW_DEFAULT)


# ----------------------------- FMZ 入口 -----------------------------


def main():
    cfg = load_config()
    store = load_store()
    Log("TabFM K线窗口预测策略启动 | symbol:{} | real_tabfm:{} | auto_trade:{}".format(
        cfg["symbol"] or "当前交易对", cfg["use_real_tabfm"], cfg["auto_trade"]
    ), "#00AAFF")

    try:
        check_runtime(cfg)
    except Exception as e:
        Log("环境检查异常(不影响运行): " + repr(e), "#FF0000")

    # 无论持久化状态如何,每次策略进程启动都立即预测一次。
    try:
        run_prediction_cycle(store, cfg, trigger="startup")
        store["last_prediction_hour"] = current_hour_key()
        save_store(store)
    except Exception as e:
        Log("启动预测异常: " + repr(e), "#FF0000")

    Log("进入主循环:每小时整点预测一次,持仓持续监控", "#00AAFF")
    while True:
        try:
            handle_command(store)
            tick(store, cfg)
            show_dashboard(store, cfg)
            save_store(store)
        except Exception as e:
            Log("主循环异常: " + repr(e), "#FF0000")
        Sleep(int(cfg["check_interval_s"]) * 1000)


# ----------------------------- 配置和状态 -----------------------------


def g(name, default):
    return globals().get(name, default)


def load_config():
    return {
        "symbol": g("SYMBOL", ""),
        "period_name": g("PERIOD_NAME", "PERIOD_M15"),
        "bar_limit": int(g("BAR_LIMIT", 260)),
        "raw_window": int(g("RAW_WINDOW", RAW_WINDOW_DEFAULT)),
        "train_rows": int(g("TRAIN_ROWS", TRAIN_ROWS_DEFAULT)),
        "return_threshold": float(g("RETURN_THRESHOLD", 0.0003)),
        "min_confidence": float(g("MIN_CONFIDENCE", 0.60)),
        "use_real_tabfm": bool(g("USE_REAL_TABFM", True)),
        "auto_trade": bool(g("AUTO_TRADE", False)),
        "risk_pct": float(g("RISK_PCT", 0.01)),
        "max_leverage": float(g("MAX_LEVERAGE", 2)),
        "hard_stop_atr": float(g("HARD_STOP_ATR", 2.0)),
        "trail_start_atr": float(g("TRAIL_START_ATR", 2.0)),
        "trail_giveback_atr": float(g("TRAIL_GIVEBACK_ATR", 1.0)),
        "reverse_on_signal": bool(g("REVERSE_ON_SIGNAL", True)),
        "check_interval_s": int(g("CHECK_INTERVAL_S", 10)),
        "cooldown_bars": int(g("COOLDOWN_BARS", 3)),
    }


def new_store():
    return {
        "last_bar_time": 0,
        "last_prediction_hour": "",
        "last_prediction_at": 0,
        "last_prediction_trigger": "",
        "last_signal_bar": 0,
        "last_prediction": None,
        "last_action": "",
        "position_state": {},
        "signals": [],
        "logs": [],
        "runtime": {},
    }


def load_store():
    store = _G(STORE_KEY)
    if not store:
        return new_store()
    fresh = new_store()
    for key, value in fresh.items():
        if key not in store:
            store[key] = value
    return store


def save_store(store):
    _G(STORE_KEY, store)


def handle_command(store):
    cmd = GetCommand()
    if not cmd:
        return
    if cmd == "clear:store":
        store.clear()
        store.update(new_store())
        save_store(store)
        Log("已清空本地状态", "#FFAA00")
    elif cmd == "close:position":
        pos = get_position()
        if pos:
            close_position(pos, "manual_command")


# ----------------------------- 包检查和安装 -----------------------------


def check_runtime(cfg):
    status = {"mode": "mock", "packages": {}}
    for import_name, pip_name in [
        ("numpy", "numpy"),
        ("pandas", "pandas"),
    ]:
        status["packages"][import_name] = ensure_package(import_name)

    if cfg["use_real_tabfm"]:
        status["packages"]["tabfm"] = ensure_tabfm()
        if status["packages"]["tabfm"]:
            status["mode"] = "real_tabfm"

    store = load_store()
    store["runtime"] = status
    save_store(store)
    Log("运行环境检查完成: " + json.dumps(status, ensure_ascii=False), "#00AAFF")


def ensure_package(import_name):
    try:
        importlib.import_module(import_name)
        return True
    except Exception as exc:
        Log("缺少包 {},请在托管者环境预先安装: {}".format(import_name, exc), "#FFAA00")
        return False


def ensure_tabfm():
    try:
        import tabfm
        from tabfm import TabFMClassifier
        from tabfm import tabfm_v1_0_0_jax

        Log(
            "检测到 TabFM JAX 后端 | tabfm路径: {}".format(
                getattr(tabfm, "__file__", "unknown")
            ),
            "#00AAFF",
        )
        return True
    except Exception as exc:
        Log(
            "未检测到可用的 TabFM JAX 后端,降级 mock: {}".format(exc),
            "#FFAA00",
        )
        return False


# ----------------------------- 主循环 -----------------------------


def current_hour_key(now_ts=None):
    """返回本地时间的小时键,例如 2026-191-10。"""
    now_ts = time.time() if now_ts is None else now_ts
    tm = time.localtime(now_ts)
    return "{:04d}-{:03d}-{:02d}".format(tm.tm_year, tm.tm_yday, tm.tm_hour)


def next_hour_timestamp(now_ts=None):
    now_ts = time.time() if now_ts is None else now_ts
    tm = time.localtime(now_ts)
    current_hour_start = time.mktime((
        tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour, 0, 0,
        tm.tm_wday, tm.tm_yday, tm.tm_isdst,
    ))
    return int(current_hour_start + 3600)


def tick(store, cfg):
    """
    主循环每 CHECK_INTERVAL_S 调用一次:
    1. 每个自然小时只触发一次 TabFM 预测;
    2. 每次循环都执行持仓监控。
    """
    hour_key = current_hour_key()

    if store.get("last_prediction_hour") != hour_key:
        # 第一次跨入新小时即执行,通常在整点后的 0~CHECK_INTERVAL_S 秒内。
        store["last_prediction_hour"] = hour_key
        save_store(store)
        run_prediction_cycle(store, cfg, trigger="hourly")

    bars = get_records(
        cfg["symbol"],
        period_value(cfg["period_name"]),
        cfg["bar_limit"],
    )

    if bars:
        monitor_position(bars, cfg, store)


def run_prediction_cycle(store, cfg, trigger):
    bars = get_records(
        cfg["symbol"],
        period_value(cfg["period_name"]),
        cfg["bar_limit"],
    )

    need = cfg["raw_window"] + cfg["train_rows"]
    if len(bars) < need:
        Log("K线数量不足: {} / {}".format(len(bars), need), "#999999")
        store["last_action"] = "prediction_skipped_insufficient_bars"
        store["last_prediction_at"] = int(time.time())
        store["last_prediction_trigger"] = trigger
        return False

    started_at = time.time()
    last_bar = bars[-1]
    store["last_bar_time"] = last_bar["Time"]

    Log(
        "开始执行 TabFM 预测 | trigger:{} | bar_time:{}".format(
            trigger, last_bar["Time"]
        ),
        "#FFAA00",
    )

    rows, labels, latest = build_dataset(
        bars,
        cfg["raw_window"],
        cfg["train_rows"],
        cfg["return_threshold"],
    )

    prediction = predict_direction(rows, labels, latest, cfg)
    prediction["trigger"] = trigger
    prediction["started_at"] = int(started_at)
    prediction["finished_at"] = int(time.time())
    prediction["elapsed_seconds"] = round(time.time() - started_at, 3)

    store["last_prediction"] = prediction
    store["last_prediction_at"] = prediction["finished_at"]
    store["last_prediction_trigger"] = trigger

    Log(
        "TabFM 预测完成 | label:{} | confidence:{:.1%} | mode:{} | 耗时:{:.1f}s".format(
            prediction["label"],
            prediction["confidence"],
            prediction["mode"],
            prediction["elapsed_seconds"],
        ),
        "#00CC66",
    )

    signal = build_signal(store, bars, prediction, cfg)
    if signal:
        store["signals"].insert(0, signal)
        store["signals"] = store["signals"][:50]
        execute_signal(signal, cfg, store)

    monitor_position(bars, cfg, store)
    return True


# ----------------------------- 预测逻辑 -----------------------------


def predict_direction(rows, labels, latest, cfg):
    use_real = cfg["use_real_tabfm"] and can_use_tabfm()
    if use_real:
        try:
            label, probs = tabfm_predict(rows, labels, latest)
            mode = "real_tabfm"
        except Exception as exc:
            Log("TabFM 预测失败,降级 mock: " + str(exc), "#FF0000")
            label, probs = mock_predict(latest)
            mode = "mock_fallback"
    else:
        label, probs = mock_predict(latest)
        mode = "mock"

    return {
        "ok": True,
        "mode": mode,
        "label": label,
        "confidence": float(probs.get(label, 0)),
        "probabilities": probs,
        "latest_features": latest,
        "context_rows": len(rows),
        "label_counts": count_labels(labels),
    }


def can_use_tabfm():
    if MODEL_CACHE["available"] is False:
        return False

    try:
        from tabfm import TabFMClassifier
        from tabfm import tabfm_v1_0_0_jax

        if TabFMClassifier is None or tabfm_v1_0_0_jax is None:
            raise ImportError("TabFM JAX 后端不可用")

        MODEL_CACHE["available"] = True
        return True

    except Exception as exc:
        MODEL_CACHE["available"] = False
        Log("TabFM JAX 后端不可用: " + str(exc), "#FFAA00")
        return False


def tabfm_predict(rows, labels, latest):
    import numpy as np
    import pandas as pd
    from tabfm import TabFMClassifier
    from tabfm import tabfm_v1_0_0_jax as tabfm_backend

    if MODEL_CACHE["model"] is None:
        Log("加载 TabFM JAX 预训练模型,首次运行可能较慢...", "#FFAA00")
        MODEL_CACHE["model"] = tabfm_backend.load(
            model_type="classification"
        )
        Log("TabFM JAX 预训练模型加载完成", "#00CC66")

    if MODEL_CACHE["classifier"] is None:
        MODEL_CACHE["classifier"] = TabFMClassifier(
            model=MODEL_CACHE["model"],
            random_state=42,
        )

    clf = MODEL_CACHE["classifier"]
    feature_columns = list(rows[0].keys())
    x_train = pd.DataFrame(rows, columns=feature_columns)
    y_train = np.asarray(labels)
    x_test = pd.DataFrame([latest], columns=feature_columns)

    # 每小时更新一次上下文;只调用 predict_proba,避免 predict 再做一次推理。
    clf.fit(x_train, y_train)
    probabilities = clf.predict_proba(x_test)[0]

    probs = {
        str(cls): float(prob)
        for cls, prob in zip(clf.classes_, probabilities)
    }

    for class_name in ("up", "flat", "down"):
        probs.setdefault(class_name, 0.0)

    total = sum(probs.values())
    if total > 0:
        probs = {key: value / total for key, value in probs.items()}

    label = max(probs, key=probs.get)
    return label, probs


def mock_predict(latest):
    # mock 只用于环境缺失时维持策略运行,不代表 TabFM 结果。
    recent_return = float(latest.get("lag_1_close", 0.0) - latest.get("lag_4_close", 0.0))
    candle_pressure = float(latest.get("lag_1_close", 0.0) - latest.get("lag_1_open", 0.0))
    score = max(-20.0, min(20.0, (recent_return + candle_pressure) * 80.0))
    up = 1.0 / (1.0 + math.exp(-score))
    down = 1.0 - up
    flat = max(0.15, 1.0 - abs(up - down))
    total = up + down + flat
    probs = {"up": up / total, "flat": flat / total, "down": down / total}
    label = max(probs, key=probs.get)
    return label, probs

def count_labels(labels):
    out = {}
    for label in labels:
        out[label] = out.get(label, 0) + 1
    return out


# ----------------------------- 特征工程 -----------------------------


def build_dataset(bars, window, train_rows, return_threshold):
    """
    用过去 window 根已完成K线预测紧接着的下一根K线。

    训练样本按“最新到最旧”排列:
    rows[0] 是最近一条已知样本,rows[-1] 是最旧样本。

    每一行内部也按“最近到最远”排列:
    lag_1 是目标K线前一根,lag_window 是更早的一根。
    """
    minimum_bars = window + train_rows
    if len(bars) < minimum_bars:
        raise ValueError(
            "K线数量不足,至少需要 {} 根,当前 {} 根".format(
                minimum_bars, len(bars)
            )
        )

    rows = []
    labels = []

    # bars[-1] 是最新一根已经完成的K线,可以作为最近训练样本的标签。
    latest_known_target_idx = len(bars) - 1

    # 从最近的已知样本开始,逐行向历史方向移动。
    for offset in range(train_rows):
        target_idx = latest_known_target_idx - offset

        rows.append(
            make_raw_kline_row(
                bars,
                target_idx=target_idx,
                window=window,
            )
        )
        labels.append(
            make_next_bar_label(
                bars,
                target_idx=target_idx,
                threshold=return_threshold,
            )
        )

    # target_idx=len(bars) 表示下一根尚未出现的K线。
    # 特征只读取 bars[-1] 到 bars[-window],不存在未来数据泄漏。
    latest = make_raw_kline_row(
        bars,
        target_idx=len(bars),
        window=window,
    )

    return rows, labels, latest


def make_next_bar_label(bars, target_idx, threshold):
    """根据目标K线相对前一根收盘价的收益生成 up/down/flat 标签。"""
    previous_close = float(bars[target_idx - 1]["Close"])
    target_close = float(bars[target_idx]["Close"])

    if previous_close <= 0:
        return "flat"

    target_return = target_close / previous_close - 1.0

    if target_return > threshold:
        return "up"
    if target_return < -threshold:
        return "down"
    return "flat"


def make_raw_kline_row(bars, target_idx, window):
    """
    使用目标K线之前的 window 根已完成K线构造一行表格。

    lag_1:距离目标最近的一根K线;
    lag_15:人工时间窗口中最早的一根K线。

    这个窗口显式保留局部时序位置,但 TabFM 仍将其视为固定列的表格,
    它不是 TimesFM 那种直接建模序列的原生时序基础模型。
    """
    anchor_close = float(bars[target_idx - 1]["Close"])
    if anchor_close <= 0:
        anchor_close = 1.0

    volumes = [
        float(bars[target_idx - lag].get("Volume", 0.0))
        for lag in range(1, window + 1)
    ]
    mean_volume = sum(volumes) / len(volumes) if volumes else 1.0
    if mean_volume <= 0:
        mean_volume = 1.0

    row = {}
    for lag in range(1, window + 1):
        bar = bars[target_idx - lag]
        prefix = "lag_{}".format(lag)

        row[prefix + "_open"] = safe_div(float(bar["Open"]) - anchor_close, anchor_close)
        row[prefix + "_high"] = safe_div(float(bar["High"]) - anchor_close, anchor_close)
        row[prefix + "_low"] = safe_div(float(bar["Low"]) - anchor_close, anchor_close)
        row[prefix + "_close"] = safe_div(float(bar["Close"]) - anchor_close, anchor_close)
        row[prefix + "_volume"] = safe_div(float(bar.get("Volume", 0.0)), mean_volume)

    return row


# ----------------------------- 交易逻辑 -----------------------------


def build_signal(store, bars, prediction, cfg):
    label = prediction["label"]
    confidence = float(prediction["confidence"])
    close = float(bars[-1]["Close"])
    atr_v = atr(bars, 14)[-1]
    bar_time = bars[-1]["Time"]

    cooldown_ms = cfg["cooldown_bars"] * period_ms(period_value(cfg["period_name"]))
    if bar_time - store.get("last_signal_bar", 0) < cooldown_ms:
        store["last_action"] = "cooldown"
        return None

    if confidence < cfg["min_confidence"]:
        store["last_action"] = "hold_low_confidence"
        return None

    if label == "up":
        direction = "long"
    elif label == "down":
        direction = "short"
    else:
        store["last_action"] = "hold_flat"
        return None

    store["last_signal_bar"] = bar_time
    return {
        "time": int(time.time()),
        "bar_time": bar_time,
        "strategy": STRATEGY_NAME,
        "direction": direction,
        "price": close,
        "atr": atr_v,
        "label": label,
        "confidence": confidence,
        "probabilities": prediction["probabilities"],
        "reason": "预测 {},置信度 {:.1%}".format(label, confidence),
    }


def execute_signal(signal, cfg, store):
    Log("交易信号: {} | conf:{:.1%} | price:{}".format(signal["direction"], signal["confidence"], signal["price"]), "#00CC66")
    pos = get_position()
    if not cfg["auto_trade"]:
        store["last_action"] = "notify_" + signal["direction"]
        return True

    if pos:
        pos_dir = position_direction(pos)
        if pos_dir == signal["direction"]:
            store["last_action"] = "skip_same_position"
            return False
        if not cfg.get("reverse_on_signal", True):
            store["last_action"] = "skip_reverse_disabled"
            return False
        close_position(pos, "reverse_signal")
        store["position_state"] = {}
        Sleep(1000)

    ok = open_position(signal, cfg)
    if ok:
        init_position_state(store, signal)
    store["last_action"] = "open_" + signal["direction"] if ok else "open_failed"
    return ok


def open_position(signal, cfg):
    acc = exchange.GetAccount()
    ticker = get_ticker(cfg["symbol"])
    if not acc or not ticker or float(ticker["Last"]) <= 0:
        return False
    last = float(ticker["Last"])
    equity = get_equity(acc)
    risk_cash = equity * cfg["risk_pct"]
    stop_distance = max(float(signal["atr"]) * cfg["hard_stop_atr"], last * 0.001)
    qty = risk_cash / stop_distance
    max_qty = equity * cfg["max_leverage"] / last
    qty = normalize_qty(max(0.0, min(qty, max_qty)))
    if qty <= 0:
        return False

    if signal["direction"] == "long":
        exchange.SetDirection("buy")
        order_id = exchange.Buy(-1, qty)
    else:
        exchange.SetDirection("sell")
        order_id = exchange.Sell(-1, qty)
    Log("下单: {} qty:{} order:{}".format(signal["direction"], qty, order_id), "#00AAFF")
    return bool(order_id)


def monitor_position(bars, cfg, store=None):
    pos = get_position()
    if not pos:
        if store is not None:
            store["position_state"] = {}
        return
    ticker = get_ticker(cfg["symbol"])
    if not ticker or float(ticker["Last"]) <= 0:
        return

    atr_v = atr(bars, 14)[-1]
    direction = position_direction(pos)
    entry = position_price(pos)
    last = float(ticker["Last"])
    state = ensure_position_state(store, pos, direction, entry, last) if store is not None else {}

    hard_stop_distance = atr_v * cfg["hard_stop_atr"]
    if direction == "long":
        state["best_price"] = max(float(state.get("best_price", entry)), last)
        hard_stop = entry - hard_stop_distance
        profit_atr = safe_div(state["best_price"] - entry, atr_v)
        trail_stop = None
        if profit_atr >= cfg["trail_start_atr"]:
            trail_stop = state["best_price"] - atr_v * cfg["trail_giveback_atr"]
            state["trail_active"] = True
        final_stop = max(hard_stop, trail_stop) if trail_stop is not None else hard_stop
        state["stop_price"] = final_stop
        if last <= final_stop:
            close_position(pos, "trailing_stop" if state.get("trail_active") else "hard_stop")
            if store is not None:
                store["last_action"] = "closed_by_stop"
                store["position_state"] = {}
            return
    else:
        state["best_price"] = min(float(state.get("best_price", entry)), last)
        hard_stop = entry + hard_stop_distance
        profit_atr = safe_div(entry - state["best_price"], atr_v)
        trail_stop = None
        if profit_atr >= cfg["trail_start_atr"]:
            trail_stop = state["best_price"] + atr_v * cfg["trail_giveback_atr"]
            state["trail_active"] = True
        final_stop = min(hard_stop, trail_stop) if trail_stop is not None else hard_stop
        state["stop_price"] = final_stop
        if last >= final_stop:
            close_position(pos, "trailing_stop" if state.get("trail_active") else "hard_stop")
            if store is not None:
                store["last_action"] = "closed_by_stop"
                store["position_state"] = {}
            return

    state["last_price"] = last
    state["atr"] = atr_v
    if store is not None:
        store["position_state"] = state


def init_position_state(store, signal):
    store["position_state"] = {
        "direction": signal["direction"],
        "entry_price": float(signal["price"]),
        "best_price": float(signal["price"]),
        "last_price": float(signal["price"]),
        "atr": float(signal["atr"]),
        "stop_price": 0.0,
        "trail_active": False,
        "opened_at": int(time.time()),
    }


def ensure_position_state(store, pos, direction, entry, last):
    if store is None:
        return {}
    state = store.get("position_state") or {}
    if state.get("direction") != direction or abs(float(state.get("entry_price", entry)) - entry) > max(entry * 0.001, 1e-8):
        state = {
            "direction": direction,
            "entry_price": entry,
            "best_price": last,
            "last_price": last,
            "atr": 0.0,
            "stop_price": 0.0,
            "trail_active": False,
            "opened_at": int(time.time()),
        }
    return state


def close_position(pos, reason):
    amount = abs(float(pos.get("Amount", pos.get("amount", 0))))
    if amount <= 0:
        return False
    direction = position_direction(pos)
    if direction == "long":
        exchange.SetDirection("closebuy")
        order_id = exchange.Sell(-1, amount)
    else:
        exchange.SetDirection("closesell")
        order_id = exchange.Buy(-1, amount)
    Log("平仓: {} amount:{} reason:{} order:{}".format(direction, amount, reason, order_id), "#FFAA00")
    return bool(order_id)


# ----------------------------- FMZ 兼容工具 -----------------------------


def get_records(symbol, period, limit):
    try:
        if symbol:
            records = exchange.GetRecords(symbol, period, limit)
        else:
            records = exchange.GetRecords(period)
    except Exception:
        records = exchange.GetRecords(period)
    if not records:
        return []
    return records[-limit:]


def get_ticker(symbol):
    try:
        if symbol:
            return exchange.GetTicker(symbol)
        return exchange.GetTicker()
    except Exception:
        return exchange.GetTicker()


def get_position():
    try:
        positions = exchange.GetPositions(SYMBOL) if SYMBOL else exchange.GetPositions()
    except Exception:
        return None
    if not positions:
        return None
    for pos in positions:
        amount = abs(float(pos.get("Amount", pos.get("amount", 0))))
        if amount > 0:
            return pos
    return None


def position_direction(pos):
    pos_type = pos.get("Type", pos.get("type"))
    if pos_type in (0, "buy", "long", "LONG"):
        return "long"
    if pos_type in (1, "sell", "short", "SHORT"):
        return "short"
    return "long" if float(pos.get("Amount", 0)) >= 0 else "short"


def position_price(pos):
    return float(pos.get("Price", pos.get("price", pos.get("AvgPrice", 0))))


def get_equity(acc):
    for key in ("Equity", "Balance", "NetAsset"):
        if key in acc and acc[key] is not None:
            return float(acc[key])
    return 0.0


def normalize_qty(qty):
    if qty <= 0:
        return 0.0
    return math.floor(qty * 10000) / 10000.0


def period_value(period_name):
    return globals().get(period_name, PERIOD_M15)


def period_ms(period):
    mapping = {
        PERIOD_M1: 60000,
        PERIOD_M5: 300000,
        PERIOD_M15: 900000,
        PERIOD_M30: 1800000,
        PERIOD_H1: 3600000,
        PERIOD_D1: 86400000,
    }
    return mapping.get(period, 900000)


# ----------------------------- 指标工具 -----------------------------


def ema(values, period):
    if not values:
        return []
    alpha = 2.0 / (period + 1.0)
    out = [float(values[0])]
    for value in values[1:]:
        out.append(alpha * float(value) + (1.0 - alpha) * out[-1])
    return out


def sma(values, period):
    out = []
    for idx in range(len(values)):
        start = max(0, idx - period + 1)
        window = values[start : idx + 1]
        out.append(sum(window) / len(window))
    return out


def rolling_std(values, period):
    out = []
    for idx in range(len(values)):
        start = max(0, idx - period + 1)
        window = values[start : idx + 1]
        mean = sum(window) / len(window)
        variance = sum((x - mean) ** 2 for x in window) / len(window)
        out.append(math.sqrt(variance))
    return out


def rsi(closes, period):
    gains = [0.0]
    losses = [0.0]
    for idx in range(1, len(closes)):
        change = closes[idx] - closes[idx - 1]
        gains.append(max(change, 0.0))
        losses.append(max(-change, 0.0))
    avg_gain = sma(gains, period)
    avg_loss = sma(losses, period)
    out = []
    for gain, loss in zip(avg_gain, avg_loss):
        if loss <= 1e-12:
            out.append(100.0 if gain > 0 else 50.0)
        else:
            out.append(100.0 - 100.0 / (1.0 + gain / loss))
    return out


def atr(bars, period):
    trs = []
    prev_close = None
    for bar in bars:
        high = float(bar["High"])
        low = float(bar["Low"])
        close = float(bar["Close"])
        if prev_close is None:
            tr = high - low
        else:
            tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
        trs.append(tr)
        prev_close = close
    return sma(trs, period)


def pct_change(values, idx, lag):
    if idx - lag < 0 or values[idx - lag] == 0:
        return 0.0
    return values[idx] / values[idx - lag] - 1.0


def safe_div(a, b):
    if abs(b) <= 1e-12:
        return 0.0
    return a / b


# ----------------------------- 展示 -----------------------------


def format_time(ts):
    if not ts:
        return "-"
    return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))


def show_dashboard(store, cfg):
    prediction = store.get("last_prediction") or {}
    probs = prediction.get("probabilities", {})
    runtime = store.get("runtime", {})
    table = {
        "type": "table",
        "title": "TabFM 原始K线预测交易示范",
        "cols": ["项目", "值"],
        "rows": [
            ["symbol", cfg["symbol"] or "当前交易对"],
            ["raw_window", cfg.get("raw_window", RAW_WINDOW_DEFAULT)],
            ["train_rows", cfg.get("train_rows", TRAIN_ROWS_DEFAULT)],
            ["mode", prediction.get("mode", runtime.get("mode", "-"))],
            ["schedule", "启动立即执行;之后每小时整点"],
            ["last_trigger", store.get("last_prediction_trigger", "-")],
            ["last_prediction_at", format_time(store.get("last_prediction_at", 0))],
            ["next_prediction_at", format_time(next_hour_timestamp())],
            ["elapsed_seconds", prediction.get("elapsed_seconds", "-")],
            ["auto_trade", cfg["auto_trade"]],
            ["label", prediction.get("label", "-")],
            ["confidence", "{:.1%}".format(float(prediction.get("confidence", 0)))],
            ["up", "{:.1%}".format(float(probs.get("up", 0)))],
            ["flat", "{:.1%}".format(float(probs.get("flat", 0)))],
            ["down", "{:.1%}".format(float(probs.get("down", 0)))],
            ["context_rows", prediction.get("context_rows", "-")],
            ["label_counts", json.dumps(prediction.get("label_counts", {}), ensure_ascii=False)],
            ["last_action", store.get("last_action", "")],
            ["trail_active", (store.get("position_state") or {}).get("trail_active", False)],
            ["best_price", (store.get("position_state") or {}).get("best_price", "-")],
            ["stop_price", (store.get("position_state") or {}).get("stop_price", "-")],
        ],
    }
    LogStatus("`" + json.dumps(table, ensure_ascii=False) + "`")
Strategy parameters
Strategy parameters
Symbol (Optional)
K-line Period
Bar Limit
Lookahead Bars
Return Threshold
Min Confidence
Use Real TabFM
Auto Trade
Risk Percent
Max Leverage
Hard Stop ATR
Trail Start ATR
Trail Giveback ATR
Reverse On Signal
Check Interval
Cooldown Bars
Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)