fix: H1 bias calculate on first loop + add filter config infrastructure
**H1 Bias Fix:** - Fixed cache check to ensure loop_count=1 always calculates H1 bias - Changed exception log from DEBUG to WARNING for visibility - Added log on first calculation (loop==1) in addition to every 4 loops - Result: H1 bias now correctly calculated from first candle **Filter Config Infrastructure (WIP):** - Added FilterConfigManager (src/filter_config.py) for dynamic filter control - Added data/filter_config.json with 11 entry filters (flash_crash, regime, risk, session, spread, h1_bias, ml_confidence, signal_combination, cooldown, time_filter, market_close) - Added API endpoints: GET/POST /api/filters/config - Note: Bot integration pending — requires wrapper around all filter checks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
71b9bcc472
commit
85161b4965
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"filters": {
|
||||||
|
"flash_crash_guard": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Flash Crash Guard",
|
||||||
|
"description": "Block entries during extreme price movements"
|
||||||
|
},
|
||||||
|
"regime_filter": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Regime Filter",
|
||||||
|
"description": "Filter based on HMM regime detection"
|
||||||
|
},
|
||||||
|
"risk_check": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Risk Check",
|
||||||
|
"description": "Daily/total loss limits validation"
|
||||||
|
},
|
||||||
|
"session_filter": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Session Filter",
|
||||||
|
"description": "Trading session (Sydney/London/NY) validation"
|
||||||
|
},
|
||||||
|
"spread_check": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Spread Check",
|
||||||
|
"description": "Block entries when spread too wide"
|
||||||
|
},
|
||||||
|
"h1_bias": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "H1 Bias Filter",
|
||||||
|
"description": "Multi-timeframe H1 EMA20 alignment (#31B)"
|
||||||
|
},
|
||||||
|
"ml_confidence": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "ML Confidence",
|
||||||
|
"description": "XGBoost confidence vs dynamic threshold"
|
||||||
|
},
|
||||||
|
"signal_combination": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Signal Combination",
|
||||||
|
"description": "SMC + ML signal agreement check"
|
||||||
|
},
|
||||||
|
"cooldown": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Cooldown Period",
|
||||||
|
"description": "Minimum time between trades (150s)"
|
||||||
|
},
|
||||||
|
"time_filter": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Time Filter",
|
||||||
|
"description": "Block specific hours (#34A: skip 9,21 WIB)"
|
||||||
|
},
|
||||||
|
"market_close_guard": {
|
||||||
|
"enabled": true,
|
||||||
|
"name": "Market Close Guard",
|
||||||
|
"description": "Block entries near daily/weekend close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"updated_at": "2026-02-09T08:05:00+07:00",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
Filter Configuration Manager
|
||||||
|
=============================
|
||||||
|
Load and save entry filter enable/disable states from data/filter_config.json.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from src.filter_config import FilterConfigManager
|
||||||
|
|
||||||
|
config = FilterConfigManager()
|
||||||
|
if config.is_enabled("h1_bias"):
|
||||||
|
# Apply H1 bias filter
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
WIB = ZoneInfo("Asia/Jakarta")
|
||||||
|
|
||||||
|
|
||||||
|
class FilterConfigManager:
|
||||||
|
"""Manage entry filter enable/disable configuration."""
|
||||||
|
|
||||||
|
def __init__(self, config_path: str = "data/filter_config.json"):
|
||||||
|
"""Initialize filter config manager."""
|
||||||
|
self.config_path = Path(config_path)
|
||||||
|
self.filters: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self.metadata: Dict[str, Any] = {}
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
"""Load filter config from JSON file."""
|
||||||
|
try:
|
||||||
|
if not self.config_path.exists():
|
||||||
|
logger.warning(f"Filter config not found at {self.config_path}, using defaults (all enabled)")
|
||||||
|
self._init_defaults()
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
self.filters = data.get("filters", {})
|
||||||
|
self.metadata = data.get("metadata", {})
|
||||||
|
|
||||||
|
enabled_count = sum(1 for f in self.filters.values() if f.get("enabled", True))
|
||||||
|
total_count = len(self.filters)
|
||||||
|
logger.info(f"Filter config loaded: {enabled_count}/{total_count} filters enabled")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load filter config: {e}")
|
||||||
|
self._init_defaults()
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
"""Save current filter config to JSON file."""
|
||||||
|
try:
|
||||||
|
self.metadata["updated_at"] = datetime.now(WIB).isoformat()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"filters": self.filters,
|
||||||
|
"metadata": self.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
logger.info(f"Filter config saved to {self.config_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save filter config: {e}")
|
||||||
|
|
||||||
|
def is_enabled(self, filter_key: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a filter is enabled.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter_key: Filter key (e.g., "h1_bias", "ml_confidence")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if enabled, False if disabled or not found
|
||||||
|
"""
|
||||||
|
if filter_key not in self.filters:
|
||||||
|
# Unknown filter — default to enabled for safety
|
||||||
|
return True
|
||||||
|
|
||||||
|
return self.filters[filter_key].get("enabled", True)
|
||||||
|
|
||||||
|
def set_enabled(self, filter_key: str, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
Enable or disable a filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter_key: Filter key
|
||||||
|
enabled: True to enable, False to disable
|
||||||
|
"""
|
||||||
|
if filter_key not in self.filters:
|
||||||
|
logger.warning(f"Unknown filter key: {filter_key}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.filters[filter_key]["enabled"] = enabled
|
||||||
|
logger.info(f"Filter '{filter_key}' {'enabled' if enabled else 'disabled'}")
|
||||||
|
|
||||||
|
def get_all(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Get all filter configurations."""
|
||||||
|
return self.filters
|
||||||
|
|
||||||
|
def update_all(self, new_config: Dict[str, bool]) -> None:
|
||||||
|
"""
|
||||||
|
Update multiple filters at once.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_config: Dict mapping filter_key -> enabled (bool)
|
||||||
|
"""
|
||||||
|
for filter_key, enabled in new_config.items():
|
||||||
|
if filter_key in self.filters:
|
||||||
|
self.filters[filter_key]["enabled"] = enabled
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
enabled_count = sum(1 for f in self.filters.values() if f.get("enabled", True))
|
||||||
|
logger.info(f"Filter config updated: {enabled_count}/{len(self.filters)} enabled")
|
||||||
|
|
||||||
|
def _init_defaults(self) -> None:
|
||||||
|
"""Initialize default filter config (all enabled)."""
|
||||||
|
self.filters = {
|
||||||
|
"flash_crash_guard": {"enabled": True, "name": "Flash Crash Guard", "description": "Block entries during extreme price movements"},
|
||||||
|
"regime_filter": {"enabled": True, "name": "Regime Filter", "description": "Filter based on HMM regime detection"},
|
||||||
|
"risk_check": {"enabled": True, "name": "Risk Check", "description": "Daily/total loss limits validation"},
|
||||||
|
"session_filter": {"enabled": True, "name": "Session Filter", "description": "Trading session validation"},
|
||||||
|
"spread_check": {"enabled": True, "name": "Spread Check", "description": "Block entries when spread too wide"},
|
||||||
|
"h1_bias": {"enabled": True, "name": "H1 Bias Filter", "description": "Multi-timeframe H1 EMA20 alignment"},
|
||||||
|
"ml_confidence": {"enabled": True, "name": "ML Confidence", "description": "XGBoost confidence threshold"},
|
||||||
|
"signal_combination": {"enabled": True, "name": "Signal Combination", "description": "SMC + ML signal agreement"},
|
||||||
|
"cooldown": {"enabled": True, "name": "Cooldown Period", "description": "Minimum time between trades"},
|
||||||
|
"time_filter": {"enabled": True, "name": "Time Filter", "description": "Block specific hours"},
|
||||||
|
"market_close_guard": {"enabled": True, "name": "Market Close Guard", "description": "Block near market close"}
|
||||||
|
}
|
||||||
|
self.metadata = {
|
||||||
|
"updated_at": datetime.now(WIB).isoformat(),
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
self.save()
|
||||||
@@ -34,6 +34,7 @@ app.add_middleware(
|
|||||||
# Status file path (mounted as volume in Docker)
|
# Status file path (mounted as volume in Docker)
|
||||||
STATUS_FILE = Path("/app/data/bot_status.json")
|
STATUS_FILE = Path("/app/data/bot_status.json")
|
||||||
MODEL_METRICS_FILE = Path("/app/data/model_metrics.json")
|
MODEL_METRICS_FILE = Path("/app/data/model_metrics.json")
|
||||||
|
FILTER_CONFIG_FILE = Path("/app/data/filter_config.json")
|
||||||
|
|
||||||
# Default empty response
|
# Default empty response
|
||||||
DEFAULT_STATUS = {
|
DEFAULT_STATUS = {
|
||||||
@@ -413,6 +414,64 @@ async def get_signal_stats(hours: int = Query(24, ge=1, le=168)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# FILTER CONFIG ENDPOINTS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@app.get("/api/filters/config")
|
||||||
|
async def get_filter_config():
|
||||||
|
"""Get current filter enable/disable states."""
|
||||||
|
try:
|
||||||
|
if not FILTER_CONFIG_FILE.exists():
|
||||||
|
return {"filters": {}, "metadata": {}}
|
||||||
|
|
||||||
|
with open(FILTER_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load filter config: {e}")
|
||||||
|
return {"filters": {}, "metadata": {}, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/filters/config")
|
||||||
|
async def update_filter_config(updates: dict):
|
||||||
|
"""
|
||||||
|
Update filter enable/disable states.
|
||||||
|
|
||||||
|
Body: { "filter_key": true/false, ... }
|
||||||
|
Example: { "h1_bias": false, "ml_confidence": true }
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Load current config
|
||||||
|
if not FILTER_CONFIG_FILE.exists():
|
||||||
|
return {"success": False, "error": "Filter config file not found"}
|
||||||
|
|
||||||
|
with open(FILTER_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Update enabled states
|
||||||
|
for filter_key, enabled in updates.items():
|
||||||
|
if filter_key in data["filters"]:
|
||||||
|
data["filters"][filter_key]["enabled"] = enabled
|
||||||
|
|
||||||
|
# Update metadata
|
||||||
|
data["metadata"]["updated_at"] = datetime.now(ZoneInfo("Asia/Jakarta")).isoformat()
|
||||||
|
|
||||||
|
# Save
|
||||||
|
with open(FILTER_CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
logger.info(f"Filter config updated: {list(updates.keys())}")
|
||||||
|
|
||||||
|
return {"success": True, "updated": list(updates.keys())}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update filter config: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
Reference in New Issue
Block a user