File size: 9,736 Bytes
3c57e36
 
 
d5ba3a3
 
3c57e36
8ed954c
 
 
 
3c57e36
 
 
d5ba3a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e813ce3
d5ba3a3
 
 
 
 
645673f
 
 
d5ba3a3
 
 
 
 
 
 
 
 
 
 
 
 
8ed954c
 
3c57e36
8ed954c
d5ba3a3
 
645673f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d5ba3a3
 
 
 
 
 
 
 
 
 
 
e813ce3
645673f
 
 
d5ba3a3
 
 
 
 
 
 
 
 
 
 
 
 
645673f
3c57e36
 
 
 
 
8ed954c
3c57e36
8ed954c
 
3c57e36
8ed954c
3c57e36
 
 
8ed954c
3c57e36
8ed954c
e813ce3
645673f
 
 
3c57e36
8ed954c
3c57e36
8ed954c
3c57e36
 
8ed954c
 
 
 
 
 
 
 
 
d5ba3a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c57e36
8ed954c
 
3c57e36
 
 
8ed954c
3c57e36
8ed954c
 
 
3c57e36
8ed954c
 
 
3c57e36
d5ba3a3
8ed954c
3c57e36
8ed954c
 
 
3c57e36
 
8ed954c
 
 
 
 
3c57e36
 
8ed954c
 
 
3c57e36
8ed954c
 
 
 
 
 
3c57e36
8ed954c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c57e36
 
 
8ed954c
3c57e36
8ed954c
 
 
 
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
import json
import os
from datetime import datetime

import requests
import yfinance as yf
from src.core.logger import get_logger
from src.core.ticker_utils import normalize_price

logger = get_logger(__name__)

PORTFOLIO_FILE = "paper_portfolio.json"

# VPS Data API (optional — falls back to local JSON if not set)
VPS_API_URL = os.getenv("VPS_API_URL", "").rstrip("/")
VPS_API_KEY = os.getenv("VPS_API_KEY", "")


def _vps_headers() -> dict:
    return {"X-API-Key": VPS_API_KEY, "Content-Type": "application/json"}


def record_paper_trade(
    ticker: str,
    entry_price: float,
    verdict: str,
    source: str,
    structured_verdict: str | None = None,
    position_size: float = 0.0,
) -> None:
    """Save a BUY/STRONG BUY/WATCH recommendation to the paper portfolio.

    When *structured_verdict* is supplied (from ``InvestmentVerdict.verdict``),
    it is used directly, skipping brittle string matching on the full report.

    For US tickers with actionable verdicts, also submits to Alpaca Paper
    Trading (if ``ALPACA_ENABLED=true``).
    """
    if structured_verdict:
        _VALID = {"STRONG BUY", "BUY", "WATCH"}
        trade_type = structured_verdict if structured_verdict in _VALID else None
    else:
        v_upper = verdict.strip().upper()
        trade_type = None
        if "STRONG BUY" in v_upper:
            trade_type = "STRONG BUY"
        elif " BUY" in v_upper or v_upper.startswith("BUY"):
            trade_type = "BUY"
        elif "WATCH" in v_upper:
            trade_type = "WATCH"

    if not trade_type:
        return

    today = datetime.now().strftime("%Y-%m-%d")

    # --- Alpaca broker execution (US equities, BUY/STRONG BUY only) ---
    order_id = None
    fill_price = None
    broker_status = "none"

    try:
        from src.broker.alpaca import calculate_order, submit_order, get_account

        acct = get_account()
        if acct and position_size > 0:
            order_params = calculate_order(
                ticker=ticker,
                verdict=trade_type,
                position_size_pct=position_size,
                account_equity=acct["equity"],
            )
            if order_params:
                result = submit_order(order_params)
                order_id = result.order_id
                fill_price = result.fill_price
                broker_status = result.broker_status
                if result.success:
                    logger.info("Alpaca order filled: %s %d shares @ %s",
                                ticker, result.qty, fill_price or "market")
                else:
                    logger.warning("Alpaca order not filled: %s — %s",
                                   ticker, result.error or broker_status)
    except Exception as exc:
        logger.warning("Alpaca execution skipped for %s: %s", ticker, exc)

    # --- Record to VPS ---
    if VPS_API_URL:
        try:
            resp = requests.post(
                f"{VPS_API_URL}/portfolio",
                headers=_vps_headers(),
                json={
                    "ticker": ticker,
                    "entry_price": entry_price,
                    "date": today,
                    "verdict": trade_type,
                    "source": source,
                    "position_size": position_size,
                    "order_id": order_id,
                    "fill_price": fill_price,
                    "broker_status": broker_status,
                },
                timeout=5,
            )
            resp.raise_for_status()
            result = resp.json()
            if result.get("status") == "duplicate":
                logger.info("Duplicate trade skipped (VPS): %s on %s", ticker, today)
            else:
                logger.info("Paper trade recorded (VPS): %s %s @ $%.2f", trade_type, ticker, entry_price)
            return
        except Exception as exc:
            logger.warning("VPS record_paper_trade failed, using local fallback: %s", exc)

    # --- Local fallback ---
    try:
        portfolio = []
        if os.path.exists(PORTFOLIO_FILE):
            with open(PORTFOLIO_FILE, "r") as f:
                portfolio = json.load(f)

        for trade in portfolio:
            if trade["ticker"] == ticker and trade["date"] == today:
                logger.info("Duplicate trade skipped: %s on %s", ticker, today)
                return

        trade = {
            "ticker": ticker,
            "entry_price": entry_price,
            "date": today,
            "verdict": trade_type,
            "source": source,
            "position_size": position_size,
            "order_id": order_id,
            "fill_price": fill_price,
            "broker_status": broker_status,
        }

        portfolio.append(trade)

        with open(PORTFOLIO_FILE, "w") as f:
            json.dump(portfolio, f, indent=4)

        logger.info("Paper trade recorded: %s %s @ $%.2f", trade_type, ticker, entry_price)

    except Exception as exc:
        logger.error("Error recording paper trade: %s", exc)


def evaluate_portfolio() -> str:
    """Read the paper portfolio and calculate live performance."""

    # Try VPS first
    if VPS_API_URL:
        try:
            resp = requests.get(
                f"{VPS_API_URL}/portfolio/evaluate",
                headers=_vps_headers(),
                timeout=30,
            )
            resp.raise_for_status()
            data = resp.json()

            if not data.get("trades"):
                return "**Paper Portfolio is empty.** The Agent hasn't tracked any stocks yet."

            report = "## PrimoGreedy Agent Track Record\n\n"
            report += "| Ticker | Date Called | Entry Price | Current Price | Return | Verdict |\n"
            report += "|--------|-------------|-------------|---------------|--------|--------|\n"

            for t in data["trades"]:
                if t["gain_pct"] is not None:
                    emoji = "+" if t["gain_pct"] > 0 else ""
                    report += (
                        f"| **{t['ticker']}** | {t['date']} | ${t['entry']:.2f} | "
                        f"${t['current']:.2f} | {emoji}{t['gain_pct']:.2f}% | {t['verdict']} |\n"
                    )
                else:
                    report += (
                        f"| **{t['ticker']}** | {t['date']} | ${t['entry']:.2f} | "
                        f"Error | N/A | {t['verdict']} |\n"
                    )

            report += f"\n### Agent Performance Summary\n"
            report += f"- **Total Calls:** {data['total_calls']}\n"
            report += f"- **Win Rate:** {data['win_rate']}%\n"
            report += f"- **Average Return per Trade:** {data['avg_return']}%\n"

            return report

        except Exception as exc:
            logger.warning("VPS evaluate_portfolio failed, using local fallback: %s", exc)

    # Local fallback (original behavior)
    return _evaluate_local()


def _evaluate_local() -> str:
    """Evaluate portfolio from local JSON file."""
    if not os.path.exists(PORTFOLIO_FILE):
        return "**Paper Portfolio is empty.** The Agent hasn't tracked any stocks yet."

    try:
        with open(PORTFOLIO_FILE, "r") as f:
            portfolio = json.load(f)

        if not portfolio:
            return "**Paper Portfolio is empty.** The Agent hasn't tracked any stocks yet."

        total_roi = 0.0
        winning_trades = 0
        valid_trades = 0

        report = "## PrimoGreedy Agent Track Record\n\n"
        report += "| Ticker | Date Called | Entry Price | Current Price | Return | Verdict |\n"
        report += "|--------|-------------|-------------|---------------|--------|--------|\n"

        for trade in portfolio:
            ticker = trade["ticker"]
            entry = trade["entry_price"]

            try:
                stock = yf.Ticker(ticker)
                info = stock.info
                price = info.get("currentPrice", 0) or info.get("regularMarketPrice", 0) or 0
                currency = info.get("currency", "USD")
                price = normalize_price(price, ticker, currency)

                if price > 0 and entry > 0:
                    gain_pct = ((price - entry) / entry) * 100
                    emoji = "+" if gain_pct > 0 else ""
                    if gain_pct > 0:
                        winning_trades += 1
                    total_roi += gain_pct
                    valid_trades += 1

                    report += (
                        f"| **{ticker}** | {trade['date']} | ${entry:.2f} | "
                        f"${price:.2f} | {emoji}{gain_pct:.2f}% | {trade['verdict']} |\n"
                    )
                else:
                    report += (
                        f"| **{ticker}** | {trade['date']} | ${entry:.2f} | "
                        f"Error | N/A | {trade['verdict']} |\n"
                    )
            except Exception as exc:
                logger.warning("Price fetch failed for %s: %s", ticker, exc)
                report += (
                    f"| **{ticker}** | {trade['date']} | ${entry:.2f} | "
                    f"Error | N/A | {trade['verdict']} |\n"
                )

        if valid_trades > 0:
            avg_roi = total_roi / valid_trades
            win_rate = (winning_trades / valid_trades) * 100
        else:
            avg_roi = 0
            win_rate = 0

        report += f"\n### Agent Performance Summary\n"
        report += f"- **Total Calls:** {len(portfolio)}\n"
        report += f"- **Win Rate:** {win_rate:.1f}%\n"
        report += f"- **Average Return per Trade:** {avg_roi:.2f}%\n"

        return report

    except Exception as exc:
        logger.error("Portfolio evaluation error: %s", exc)
        return f"Error reading portfolio: {exc}"