# source: https://raw.githubusercontent.com/witooh/else/91af2afc0b8431ec043dceb27d5acbc122c5765b/user_data/strategies/ValueAccumulator.py
"""
Github_witooh_else__ValueAccumulator__20260305_161013 — Value Averaging BTC Accumulation

Strategy:
  Entry:     Always open (no indicator gate) — VA gap governs amount
  Routine:   25th of each month — buy to close gap between target_value and current portfolio value
  Emergency: Single-day drop >15% — Sniper buy 3x from vault (floor $500)
  Reserve:   Vault accumulates salary; deployed to close gap; cooldown 7d
  Hold:      Never sell (stoploss -99%)

Buy calculation (every 25th):
  vault        += monthly_inflow         ($840 received)
  target_value += monthly_inflow         (target path advances)
  current_value = trade.amount × price   (mark-to-market)
  gap           = target_value − current_value
  if gap <= 0:  skip (vault accumulates)
  if gap > 0:   buy = min(gap, base_amount × max_multiplier, vault)

Config overrides (config.json):
  "monthly_inflow": 840    # salary added each month
  "base_amount": 600       # 1x; hard cap = max_multiplier × base_amount
  "reserve_floor": 500     # minimum vault for flash crash buys
  "max_multiplier": 3.0    # hard cap multiplier (default 3x = $1,800)

Vault note: state is persisted to user_data/valueaccumulator_state.json in live/dryrun.
           Skipped during backtesting to avoid cross-run contamination.
"""

import json
from datetime import datetime
from pathlib import Path

import pandas as pd
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy


class Github_witooh_else__ValueAccumulator__20260305_161013(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "1d"
    startup_candle_count = 250  # match EDCA warmup for fair backtest comparison

    can_short = False

    stoploss = -0.99  # effectively disabled — hold through bear markets
    trailing_stop = False
    use_custom_stoploss = False

    minimal_roi = {"0": 100000}  # never hit

    use_exit_signal = False
    process_only_new_candles = True

    position_adjustment_enable = True
    max_entry_position_adjustment = -1  # unlimited DCA

    # Overrideable via config
    monthly_inflow: float = 840.0
    base_amount: float = 600.0
    reserve_floor: float = 500.0
    max_multiplier: float = 3.0
    emergency_cooldown_days: int = 7
    routine_day: int = 25

    def bot_start(self, **kwargs) -> None:
        self.monthly_inflow = self.config.get("monthly_inflow", self.monthly_inflow)
        self.base_amount    = self.config.get("base_amount",    self.base_amount)
        self.reserve_floor  = self.config.get("reserve_floor",  self.reserve_floor)
        self.max_multiplier = self.config.get("max_multiplier", self.max_multiplier)

        # Persist state to disk in live/dryrun; skip during backtest/hyperopt
        runmode = self.config.get("runmode", RunMode.OTHER)
        self._persist = runmode in (RunMode.LIVE, RunMode.DRY_RUN)
        self._state_file = Path(self.config["user_data_dir"]) / "valueaccumulator_state.json"

        self._vault: float = 0.0
        self._target_value: float = 0.0
        self._last_routine_month: int = 0
        self._last_emergency_date: datetime | None = None
        self._last_reminder_month: int = 0
        self._last_summary_week: int = 0

        if self._persist:
            self._load_state()

    def _load_state(self) -> None:
        if not self._state_file.exists():
            return
        try:
            data = json.loads(self._state_file.read_text())
            self._vault               = float(data.get("vault", 0.0))
            self._target_value        = float(data.get("target_value", 0.0))
            self._last_routine_month  = int(data.get("last_routine_month", 0))
            raw_date = data.get("last_emergency_date")
            self._last_emergency_date = datetime.fromisoformat(raw_date) if raw_date else None
        except Exception:
            pass  # corrupt file — start fresh

    def _save_state(self) -> None:
        if not self._persist:
            return
        data = {
            "vault":                self._vault,
            "target_value":         self._target_value,
            "last_routine_month":   self._last_routine_month,
            "last_emergency_date":  self._last_emergency_date.isoformat() if self._last_emergency_date else None,
        }
        self._state_file.write_text(json.dumps(data, indent=2))

    def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
        year_month = current_time.year * 100 + current_time.month
        iso_year, iso_week, _ = current_time.isocalendar()
        year_week = iso_year * 100 + iso_week

        if current_time.day == self.routine_day and year_month != self._last_reminder_month:
            self._last_reminder_month = year_month
            self._send_reminder(current_time)

        if current_time.isoweekday() == 7 and year_week != self._last_summary_week:
            self._last_summary_week = year_week
            self._send_weekly_summary(current_time)

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _get_open_trade(self):
        trades = Trade.get_trades_proxy(is_open=True)
        return trades[0] if trades else None

    def _get_current_rate(self, pair: str) -> float | None:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is not None and not dataframe.empty:
            return float(dataframe.iloc[-1]["close"])
        return None

    def _send_reminder(self, current_time: datetime) -> None:
        trade = self._get_open_trade()
        vault_line  = f"\nVault: ${self._vault:.0f}"
        target_line = f"\nTarget: ${self._target_value:.0f}"
        if trade:
            current_rate = self._get_current_rate(trade.pair)
            profit_line = ""
            if current_rate and trade.amount > 0:
                avg_cost = trade.stake_amount / trade.amount
                profit_pct = ((current_rate / avg_cost) - 1) * 100
                profit_line = f"\nกำไร: {profit_pct:+.1f}%"
            msg = (
                f"*Value Accumulator Reminder*\n"
                f"เงินเดือนเข้าแล้ว — DCA วันที่ {self.routine_day}\n"
                f"ต้นทุนสะสม: ${trade.stake_amount:.0f}"
                f"{profit_line}"
                f"{target_line}"
                f"{vault_line}"
            )
        else:
            msg = (
                f"*Value Accumulator Reminder*\n"
                f"เงินเดือนเข้าแล้ว — รอเปิด trade แรก"
                f"{target_line}"
                f"{vault_line}"
            )
        self.dp.send_msg(msg)

    def _send_weekly_summary(self, current_time: datetime) -> None:
        trade = self._get_open_trade()
        if not trade:
            return
        current_rate = self._get_current_rate(trade.pair)
        if not current_rate:
            return
        days_open = (current_time - trade.open_date_utc).days
        current_value = trade.amount * current_rate
        avg_cost = trade.stake_amount / trade.amount
        profit_pct = ((current_rate / avg_cost) - 1) * 100
        msg = (
            f"*Value Accumulator Weekly Summary*\n"
            f"สะสมมา {days_open // 30} เดือน ({days_open} วัน)\n"
            f"ต้นทุน: ${trade.stake_amount:.0f} → มูลค่า: ${current_value:.0f} ({profit_pct:+.1f}%)\n"
            f"BTC: {trade.amount:.6f}\n"
            f"Target: ${self._target_value:.0f}\n"
            f"Vault: ${self._vault:.0f}"
        )
        self.dp.send_msg(msg)

    # ------------------------------------------------------------------
    # Freqtrade interface
    # ------------------------------------------------------------------

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe["daily_change"] = dataframe["close"].pct_change()
        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe.loc[dataframe["volume"] > 0, "enter_long"] = 1
        dataframe.loc[dataframe["volume"] > 0, "enter_tag"]  = "va_init"
        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        return dataframe

    def adjust_trade_position(
        self,
        trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        min_stake,
        max_stake: float,
        current_entry_rate: float,
        current_exit_rate: float,
        current_entry_profit: float,
        current_exit_profit: float,
        **kwargs,
    ):
        """
        Two buy paths:
          1. Emergency Sniper — fires on flash crash (>15% single-day drop)
          2. Routine VA Buy — fires on day 25, closes gap between target and current value

        Vault mechanics:
          - Salary ($840) added to vault on day 25
          - Target advances by monthly_inflow on day 25
          - Routine buy draws from vault (no floor)
          - Emergency buy draws from vault above $500 floor
          - Emergency supersedes routine if both conditions met same day
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        if dataframe is None or dataframe.empty:
            return None

        last         = dataframe.iloc[-1]
        daily_change = last.get("daily_change")
        year_month   = current_time.year * 100 + current_time.month

        # Step 1: Credit vault + advance target on routine day (MUST be first)
        is_routine_day = (
            current_time.day == self.routine_day
            and year_month != self._last_routine_month
        )
        if is_routine_day:
            self._vault        += self.monthly_inflow
            self._target_value += self.monthly_inflow
            self._last_routine_month = year_month
            self._save_state()

        # Step 2: Flash Crash (highest priority — return immediately)
        emergency_eligible = (
            not pd.isna(daily_change)
            and daily_change <= -0.15
            and self._vault > self.reserve_floor
            and (
                self._last_emergency_date is None
                or (current_time - self._last_emergency_date).days >= self.emergency_cooldown_days
            )
        )
        if emergency_eligible:
            emergency_amount = self.base_amount * 3.0
            available        = self._vault - self.reserve_floor
            actual_buy       = min(emergency_amount, available, max_stake)
            if actual_buy >= (min_stake or 0):
                self._vault -= actual_buy
                self._last_emergency_date = current_time
                self._save_state()
                return actual_buy
            return None

        # Step 3: Routine VA buy (only on routine day)
        if not is_routine_day:
            return None

        current_value = trade.amount * current_rate   # mark-to-market in USDT
        gap           = self._target_value - current_value

        if gap <= 0:
            return None   # portfolio above target — vault accumulates

        hard_cap   = self.base_amount * self.max_multiplier
        actual_buy = min(gap, hard_cap, self._vault, max_stake)
        if actual_buy >= (min_stake or 0):
            self._vault -= actual_buy
            self._save_state()
            return actual_buy

        return None
