← Back
"""
WT Bot v3 — TraderMakeMoney Journal Integration
=================================================
Auto-tags trades in TMM with strategy, entry reasons.
Pulls stats from TMM (source of truth).

API: https://tradermake.money/api/v2/docs/v2
Gotchas from v2:
- notes field is `desc` not `content`
- date field is `note_at`
- TMM may take 3-15s to import trade from Binance → retry 5x
"""

import logging
import os
import time
from datetime import datetime, timezone, timedelta
from typing import Optional
from zoneinfo import ZoneInfo

import requests

logger = logging.getLogger("tmm")

VANCOUVER_TZ = ZoneInfo("America/Vancouver")

TMM_API_KEY = os.environ.get("TMM_API_KEY", "")
TMM_BASE_URL = "https://tradermake.money/api/v2"
TMM_API_KEY_ID = int(os.environ.get("TMM_API_KEY_ID", "276317"))

# Tag column IDs
TAG_COL_STRATEGY = int(os.environ.get("TMM_TAG_COL_STRATEGY", "10"))
TAG_COL_ENTRY = 1     # "Причины входа"


class TMMClient:
    def __init__(self):
        self.api_key = TMM_API_KEY
        self.session = requests.Session()
        self.session.headers.update({
            "API-KEY": self.api_key,
            "Content-Type": "application/json",
        })
        self.enabled = bool(self.api_key)
        self._pending_tags = []
        self._trade_cache = {}

        if self.enabled:
            logger.info("TMM integration enabled")
        else:
            logger.warning("TMM disabled (no API key)")

    # ============================================================
    # HTTP
    # ============================================================

    def _get(self, path, params=None):
        if not self.enabled:
            return None
        try:
            resp = self.session.get(f"{TMM_BASE_URL}{path}", params=params, timeout=10)
            resp.raise_for_status()
            return resp.json()
        except Exception as e:
            logger.error(f"TMM GET {path}: {e}")
            return None

    def _post(self, path, data=None):
        if not self.enabled:
            return None
        try:
            resp = self.session.post(f"{TMM_BASE_URL}{path}", json=data, timeout=10)
            resp.raise_for_status()
            return resp.json()
        except Exception as e:
            logger.error(f"TMM POST {path}: {e}")
            return None

    # ============================================================
    # FIND TRADE
    # ============================================================

    def find_recent_trade(self, symbol, side):
        """Find most recent TMM trade for symbol+side (within 5 min)."""
        cache_key = f"{symbol}_{side}"
        if cache_key in self._trade_cache:
            return self._trade_cache[cache_key]

        result = self._get("/trades", params={
            "page": 1, "itemsPerPage": 20,
            "sortBy": "open_time", "sortDesc": "true",
        })
        if not result or "data" not in result:
            return None

        now_ms = int(time.time() * 1000)
        for trade in result["data"]:
            if trade["symbol"] != symbol:
                continue
            t_side = trade["side"].upper()
            if (t_side == "LONG" and side != "BUY") or (t_side == "SHORT" and side != "SELL"):
                continue
            # Within 5 min
            if abs(trade["open_time"] - now_ms) <= 300_000:
                self._trade_cache[cache_key] = trade["id"]
                return trade["id"]

        return None

    # ============================================================
    # TAG & DESCRIBE
    # ============================================================

    def tag_trade(self, trade_id, tag_name, column=TAG_COL_STRATEGY):
        """Add tag to trade."""
        result = self._post(f"/trades/{trade_id}/tags", {
            "tags": [{"name": tag_name, "column": column}],
        })
        if result and result.get("status") == "success":
            logger.info(f"TMM: tagged #{trade_id} → '{tag_name}'")
            return True
        return False

    def update_description(self, trade_id, description):
        """Update trade notes/description."""
        result = self._post(f"/trades/{trade_id}/update", {
            "description": description,
        })
        if result and result.get("status") == "success":
            logger.debug(f"TMM: description updated #{trade_id}")
            return True
        return False

    # ============================================================
    # ON TRADE OPENED — auto-tag with retry
    # ============================================================

    def on_trade_opened(self, symbol, side, wt1, wt2, zone, atr_pct=0, ema200=0, price=0):
        """
        Called when bot opens a trade.
        Tags: strategy "WT_v3", entry reason.
        Description: WT values, ATR, EMA200, zone.
        """
        if not self.enabled:
            return

        # Build entry reason
        zone_str = "Oversold → Long" if zone == 1 else "Overbought → Short"
        entry_reason = f"WT Cross {zone_str}"

        # Description with all context
        description = (
            f"S3_EMA_Filter 5m | {zone_str}\n"
            f"WT1={wt1:.1f} WT2={wt2:.1f}\n"
            f"EMA200={'above' if zone == 1 else 'below'}\n"
            f"ATR={atr_pct:.1f}%"
        )

        # НЕ блокируем — сразу в очередь с задержкой 3 сек
        order_side = "BUY" if side == "LONG" else "SELL"
        logger.info(f"TMM: queuing tag for {symbol} {order_side}")
        self._pending_tags.append({
            "symbol": symbol, "side": order_side,
            "entry_reason": entry_reason, "description": description,
            "attempts": 0, "next_retry": time.time() + 3,
        })

    def _apply_tags(self, trade_id, entry_reason, description):
        """Apply strategy tag + entry reason + description."""
        # Strategy tag
        self.tag_trade(trade_id, "WT_v3", TAG_COL_STRATEGY)
        # Entry reason
        self.tag_trade(trade_id, entry_reason, TAG_COL_ENTRY)
        # Description
        self.update_description(trade_id, description)
        # Чистим кэш чтоб повторные сделки на том же символе не конфликтовали
        stale_keys = [k for k, v in self._trade_cache.items() if v == trade_id]
        for k in stale_keys:
            del self._trade_cache[k]

    def retry_pending_tags(self):
        """Retry tagging trades that weren't found immediately. Call from main loop."""
        if not self._pending_tags:
            return

        now = time.time()
        remaining = []
        for item in self._pending_tags:
            if now < item["next_retry"]:
                remaining.append(item)
                continue

            trade_id = self.find_recent_trade(item["symbol"], item["side"])
            if trade_id:
                self._apply_tags(trade_id, item["entry_reason"], item["description"])
            elif item["attempts"] < 5:
                item["attempts"] += 1
                item["next_retry"] = now + 15
                remaining.append(item)
            else:
                logger.warning(f"TMM: gave up tagging {item['symbol']} after 5 attempts")

        self._pending_tags = remaining

    # ============================================================
    # STATS FROM TMM (source of truth)
    # ============================================================

    def get_today_summary(self):
        """Today's summary from TMM."""
        today = datetime.now(VANCOUVER_TZ).strftime("%Y-%m-%d")
        trades = self._get_trades(today, today)
        return self._format_summary(trades, f"📊 Сегодня ({today})")

    def get_weekly_summary(self):
        """Current week summary."""
        now = datetime.now(VANCOUVER_TZ)
        monday = now - timedelta(days=now.weekday())
        sunday = monday + timedelta(days=6)
        trades = self._get_trades(monday.strftime("%Y-%m-%d"), sunday.strftime("%Y-%m-%d"))
        return self._format_summary(trades, f"📊 Неделя ({monday.strftime('%d.%m')}→{sunday.strftime('%d.%m')})")

    def get_total_summary(self):
        """All-time summary."""
        trades = self._get_trades("2020-01-01", "2099-12-31")
        return self._format_summary(trades, "📊 Всего (WT_v3)")

    def _get_trades(self, date_from, date_to):
        result = self._get("/trades", params={
            "api_key_id": TMM_API_KEY_ID,
            "date_from": date_from, "date_to": date_to,
            "page": 1, "itemsPerPage": 200,
            "sortBy": "open_time", "sortDesc": "true",
        })
        if result and "data" in result:
            return result["data"]
        return []

    def _format_summary(self, trades, title):
        if not trades:
            return f"{title}\n📭 Нет сделок"

        closed = [t for t in trades if int(t.get("close_time", 0)) > 0]
        open_t = [t for t in trades if int(t.get("close_time", 0)) == 0]

        if not closed:
            msg = f"{title}\n📭 Нет закрытых сделок"
            if open_t:
                msg += f"\n⏳ Открыто: {len(open_t)}"
            return msg

        total_pnl = sum(float(t.get("net_profit", 0)) for t in closed)
        wins = sum(1 for t in closed if float(t.get("net_profit", 0)) > 0)
        losses = len(closed) - wins
        wr = wins / len(closed) * 100 if closed else 0

        emoji = "🟢" if total_pnl >= 0 else "🔴"
        lines = [
            title,
            f"{emoji} PnL: ${total_pnl:+.2f}",
            f"📈 Сделок: {len(closed)} ({wins}W/{losses}L)",
            f"🎯 WR: {wr:.0f}%",
        ]

        if closed:
            best = max(closed, key=lambda t: float(t.get("net_profit", 0)))
            worst = min(closed, key=lambda t: float(t.get("net_profit", 0)))
            lines.append(f"🏆 Best: {best['symbol']} ${float(best['net_profit']):+.2f}")
            if len(closed) > 1:
                lines.append(f"💩 Worst: {worst['symbol']} ${float(worst['net_profit']):+.2f}")

        if open_t:
            lines.append(f"⏳ Открыто: {len(open_t)}")

        return "\n".join(lines)

📜 Git History

c6f6bd5chore: initial commit — version control setup5 weeks ago
Show last diff
Loading...