FactorSignalStrategy
♡
Basics
mode: spot
timeframe: 1h
interface version: 3
Settings
stoploss: -0.05
has minimal roi
process only new candles
startup candle count: 0
Other
psycopg2
yaml
15 related strategies (⧉ identical code, ≈ similar name)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | """FactorSignalStrategy — consume pre-computed factor signals from Postgres. Reads ``quant.mart_hourly_signals`` (produced by the dbt project at ``/home/zp/airbyte/quant_warehouse``) and trades the top-ranked pairs. Entry: rank_in_date <= 3 AND composite_score > 0.5 Exit : rank_in_date > 5 Single-factor mode: set QUANT_FACTOR_NAME env var to a z-score column suffix (e.g. "momentum_24h" → reads z_mom24 from factors.yml zscore_column mapping). The orchestrator validates factor names against factors.yml before spawning. """ from __future__ import annotations import logging import os from pathlib import Path from typing import Any import pandas as pd import psycopg2 import yaml # type: ignore[import-untyped] from pandas import DataFrame from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) DEFAULT_PG_DSN = "host=localhost port=5433 dbname=warehouse user=postgres password=postgres" # Default composite mode SQL _COMPOSITE_SQL = """ SELECT pair, date, composite_score, rank_in_date FROM quant.mart_hourly_signals ORDER BY date, rank_in_date """ def _load_factor_zscore_map() -> dict[str, str]: """Load factor_name → zscore_column mapping from factors.yml.""" yml_path = Path(__file__).resolve().parents[1] / "factors.yml" if not yml_path.exists(): return {} with yml_path.open() as f: data = yaml.safe_load(f) return {f["name"]: f["zscore_column"] for f in data.get("factors", [])} def _build_signals_sql(factor_name: str | None) -> str: """Build the SQL for loading signals, supporting single-factor override.""" if not factor_name: return _COMPOSITE_SQL zscore_map = _load_factor_zscore_map() if factor_name not in zscore_map: raise ValueError( f"QUANT_FACTOR_NAME={factor_name!r} not found in factors.yml. " f"Valid names: {sorted(zscore_map.keys())}" ) zcol = zscore_map[factor_name] # Safe: zcol comes from a checked-in YAML file, not user input return f""" SELECT pair, date, {zcol} AS composite_score, RANK() OVER ( PARTITION BY date ORDER BY {zcol} DESC NULLS LAST ) AS rank_in_date FROM quant.mart_hourly_signals WHERE {zcol} IS NOT NULL ORDER BY date, rank_in_date """ class FactorSignalStrategy(IStrategy): INTERFACE_VERSION = 3 timeframe = "1h" stoploss = -0.05 minimal_roi = {"0": 10.0} # effectively no ROI target — exit via rank rule process_only_new_candles = True startup_candle_count: int = 0 can_short = False # Populated by bot_start() _signals_df: pd.DataFrame | None = None # --- lifecycle ----------------------------------------------------------- def bot_start(self, **kwargs: Any) -> None: """Load the full mart_hourly_signals table once at startup.""" dsn = os.environ.get("QUANT_PG_DSN", DEFAULT_PG_DSN) factor_name = os.environ.get("QUANT_FACTOR_NAME") sql = _build_signals_sql(factor_name) mode = f"single-factor({factor_name})" if factor_name else "composite" logger.info("FactorSignalStrategy: loading signals [%s] from Postgres", mode) with psycopg2.connect(dsn) as conn: with conn.cursor() as cur: cur.execute(sql) rows = cur.fetchall() cols = [d[0] for d in cur.description] df = pd.DataFrame(rows, columns=cols) if not df.empty: df["date"] = pd.to_datetime(df["date"], utc=True) df["composite_score"] = df["composite_score"].astype(float) df["rank_in_date"] = df["rank_in_date"].astype("Int64") self._signals_df = df logger.info( "FactorSignalStrategy: loaded %d signal rows across %d pairs", len(df), df["pair"].nunique() if not df.empty else 0, ) # --- indicators / signals ------------------------------------------------ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: pair = metadata["pair"] signals = self._signals_df if signals is None or signals.empty: dataframe["composite_score"] = pd.NA dataframe["rank_in_date"] = pd.NA return dataframe pair_signals = signals.loc[ signals["pair"] == pair, ["date", "composite_score", "rank_in_date"] ] merged = dataframe.merge(pair_signals, on="date", how="left") return merged def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: rank = dataframe["rank_in_date"] score = dataframe["composite_score"] cond = rank.notna() & score.notna() & (rank <= 3) & (score > 0.5) dataframe["enter_long"] = cond.astype(int) return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: rank = dataframe["rank_in_date"] cond = rank.notna() & (rank > 5) dataframe["exit_long"] = cond.astype(int) return dataframe |
Strategy League — fixed backtest that feeds the ranking
Failed — strategy imports unavailable module: psycopg2
ata directory: /freqle/user_data ... 2026-07-28 05:26:03,135 - freqtrade.configuration.configuration - INFO - Using data directory: /freqle/user_data/data/binance ... 2026-07-28 05:26:03,135 - freqtrade.configuration.configuration - INFO - Parameter --export detected: none ... 2026-07-28 05:26:03,136 - freqtrade.configuration.configuration - INFO - Parameter --cache=none detected ... 2026-07-28 05:26:03,136 - freqtrade.configuration.configuration - INFO - Filter trades by timerange: 20210101-20260101 2026-07-28 05:26:03,137 - freqtrade.exchange.check_exchange - INFO - Checking exchange... 2026-07-28 05:26:03,143 - freqtrade.exchange.check_exchange - INFO - Exchange "binance" is officially supported by the Freqtrade development team. 2026-07-28 05:26:03,144 - freqtrade.configuration.configuration - INFO - Using pairlist from configuration. 2026-07-28 05:26:03,144 - freqtrade.configuration.config_validation - INFO - Validating configuration ... 2026-07-28 05:26:03,146 - freqtrade.exchange.exchange - INFO - Instance is running with dry_run enabled 2026-07-28 05:26:03,146 - freqtrade.exchange.exchange - INFO - Using CCXT 4.5.61 2026-07-28 05:26:03,158 - freqtrade.exchange.exchange - INFO - Using Exchange "Binance" 2026-07-28 05:26:03,346 - freqtrade.resolvers.exchange_resolver - INFO - Using resolved exchange 'Binance'... 2026-07-28 05:26:03,348 - freqtrade.resolvers.iresolver - WARNING - Could not import /freqle/user_data/strategies/FactorSignalStrategy.py due to 'No module named 'psycopg2'' 2026-07-28 05:26:03,349 - freqtrade.resolvers.iresolver - WARNING - Could not import /freqle/user_data/strategies/FactorSignalStrategy.py due to 'No module named 'psycopg2'' 2026-07-28 05:26:03,351 - freqtrade.resolvers.iresolver - WARNING - Could not import /freqle/user_data/strategies/FactorSignalStrategy.py due to 'No module named 'psycopg2'' ft_backtest wrapper failed: Impossible to load Strategy 'FactorSignalStrategy'. This class does not exist or contains Python code errors.
Backtests — over a market period
Backtest this strategy over a chosen crypto-cycle period. These don't affect the League ranking, and need that period's candle data downloaded.
Log in or sign up to run backtests.
| Period | Range | Total % | Win % | Max DD | Trades | |
|---|---|---|---|---|---|---|
| 2020 · DeFi Summer & Pre-Halving Rally | 20200101-20210101 | not run | ||||
| 2021 · Institutional Bull Market | 20210101-20220101 | not run | ||||
| 2022 · Post-Bull Crash & Macro Tightening | 20220101-20230101 | not run | ||||
| 2023–2024 · Recovery & ETF Anticipation | 20230101-20250101 | not run | ||||
| 2025–2026 · Current Cycle | 20250101-20260101 | not run | ||||
Walk forward
Out-of-sample backtest on recent data · 33 pairs · 20260101-20260701.
Backtest trust check
no lookahead-bias patterns detected
ran by Ron · took s
Lookahead analysis
freqtrade lookahead-analysis: detects strategies peeking at future candles.