Skip to content

Commit 4bf23d4

Browse files
Merge pull request #26 from Frostbite1536/claude/expert-bug-audit-prompt-CmI3w
Claude/expert bug audit prompt cm i3w
2 parents 916c300 + 65078ba commit 4bf23d4

File tree

8 files changed

+785
-24
lines changed

8 files changed

+785
-24
lines changed

EXPERT_BUG_AUDIT_FINDINGS.md

Lines changed: 409 additions & 0 deletions
Large diffs are not rendered by default.

EXPERT_BUG_AUDIT_PROMPT.md

Lines changed: 321 additions & 0 deletions
Large diffs are not rendered by default.

prediction_analyzer/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .trade_loader import Trade
1010

1111

12-
def _normalize_datetime(dt) -> datetime:
12+
def _normalize_datetime(dt) -> Optional[datetime]:
1313
"""
1414
Normalize a datetime value to a naive datetime for consistent comparison.
1515
Handles both timezone-aware and naive datetimes, and numeric timestamps.

prediction_analyzer/pnl.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from decimal import Decimal
77
from typing import List, Dict
88
import pandas as pd
9-
from .trade_loader import Trade
9+
from .trade_loader import Trade, sanitize_numeric
1010
from .inference import detect_market_resolution
1111

1212

@@ -38,16 +38,17 @@ def calculate_pnl(trades: List[Trade]) -> pd.DataFrame:
3838
cumulative.append(float(running))
3939
df["cumulative_pnl"] = cumulative
4040

41-
# Calculate exposure (net shares held)
42-
df["exposure"] = 0.0
43-
cumulative_shares = 0.0
41+
# Calculate exposure (net shares held) using Decimal to avoid float drift
42+
exposure_values = []
43+
cumulative_shares = Decimal("0")
4444

4545
for idx, row in df.iterrows():
4646
if row["type"] in ["Buy", "Market Buy", "Limit Buy"]:
47-
cumulative_shares += row["shares"]
47+
cumulative_shares += Decimal(str(row["shares"]))
4848
elif row["type"] in ["Sell", "Market Sell", "Limit Sell"]:
49-
cumulative_shares -= row["shares"]
50-
df.at[idx, "exposure"] = cumulative_shares
49+
cumulative_shares -= Decimal(str(row["shares"]))
50+
exposure_values.append(float(cumulative_shares))
51+
df["exposure"] = exposure_values
5152

5253
return df
5354

@@ -170,10 +171,12 @@ def calculate_global_pnl_summary(trades: List[Trade]) -> Dict:
170171
if len(sources) > 1:
171172
for source in sources:
172173
source_trades = [t for t in trades if getattr(t, "source", "limitless") == source]
173-
source_pnl = sum(t.pnl for t in source_trades)
174+
source_pnl = Decimal("0")
175+
for t in source_trades:
176+
source_pnl += Decimal(str(t.pnl))
174177
by_source[source] = {
175178
"total_trades": len(source_trades),
176-
"total_pnl": source_pnl,
179+
"total_pnl": float(source_pnl),
177180
"currency": (
178181
getattr(source_trades[0], "currency", "USD") if source_trades else "USD"
179182
),
@@ -190,7 +193,10 @@ def calculate_market_pnl(trades: List[Trade]) -> Dict[str, Dict]:
190193
Returns:
191194
Dictionary mapping market_slug to PnL statistics
192195
"""
193-
market_stats = {}
196+
market_stats: Dict[str, Dict] = {}
197+
# Use Decimal accumulators to avoid float drift, keyed by slug
198+
_volume_acc: Dict[str, Decimal] = {}
199+
_pnl_acc: Dict[str, Decimal] = {}
194200

195201
for trade in trades:
196202
slug = trade.market_slug
@@ -201,11 +207,17 @@ def calculate_market_pnl(trades: List[Trade]) -> Dict[str, Dict]:
201207
"total_pnl": 0.0,
202208
"trade_count": 0,
203209
}
210+
_volume_acc[slug] = Decimal("0")
211+
_pnl_acc[slug] = Decimal("0")
204212

205-
market_stats[slug]["total_volume"] += trade.cost
206-
market_stats[slug]["total_pnl"] += trade.pnl
213+
_volume_acc[slug] += Decimal(str(trade.cost))
214+
_pnl_acc[slug] += Decimal(str(trade.pnl))
207215
market_stats[slug]["trade_count"] += 1
208216

217+
for slug in market_stats:
218+
market_stats[slug]["total_volume"] = sanitize_numeric(float(_volume_acc[slug]))
219+
market_stats[slug]["total_pnl"] = sanitize_numeric(float(_pnl_acc[slug]))
220+
209221
return market_stats
210222

211223

prediction_analyzer/providers/kalshi.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from typing import List, Optional, Dict, Any
2020

2121
from .base import MarketProvider
22-
from ..trade_loader import Trade, _parse_timestamp
22+
from ..trade_loader import Trade, _parse_timestamp, sanitize_numeric
2323

2424
logger = logging.getLogger(__name__)
2525

@@ -176,14 +176,16 @@ def _fetch_position_pnl(self, api_key: str) -> Dict[str, float]:
176176

177177
for pos in data.get("market_positions", []):
178178
ticker = pos.get("ticker", "")
179-
pnl_map[ticker] = float(pos.get("realized_pnl_dollars", "0"))
179+
pnl_map[ticker] = sanitize_numeric(float(pos.get("realized_pnl_dollars", "0")))
180180

181181
cursor = data.get("cursor", "")
182182
if not cursor:
183183
break
184184

185185
return pnl_map
186186

187+
_SELL_TYPES = {"sell", "market sell", "limit sell"}
188+
187189
@staticmethod
188190
def _apply_position_pnl(trades: List[Trade], pnl_map: Dict[str, float]):
189191
"""Distribute realized PnL from positions across sell trades,
@@ -192,11 +194,11 @@ def _apply_position_pnl(trades: List[Trade], pnl_map: Dict[str, float]):
192194

193195
sell_shares: Dict[str, float] = defaultdict(float)
194196
for t in trades:
195-
if t.type.lower() == "sell" and t.market_slug in pnl_map:
197+
if t.type.lower() in KalshiProvider._SELL_TYPES and t.market_slug in pnl_map:
196198
sell_shares[t.market_slug] += t.shares
197199

198200
for t in trades:
199-
if t.type.lower() == "sell" and t.market_slug in pnl_map:
201+
if t.type.lower() in KalshiProvider._SELL_TYPES and t.market_slug in pnl_map:
200202
total = sell_shares[t.market_slug]
201203
if total > 0:
202204
t.pnl = pnl_map[t.market_slug] * (t.shares / total)
@@ -266,7 +268,7 @@ def normalize_trade(self, raw: dict, **kwargs) -> Trade:
266268
)
267269
price = 0.0
268270

269-
count_str = raw.get("count_fp", str(raw.get("count", 0)))
271+
count_str = raw.get("count_fp") or str(raw.get("count") or 0)
270272
try:
271273
count = float(count_str)
272274
except (ValueError, TypeError):

prediction_analyzer/tax.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,14 @@ def calculate_capital_gains(
115115
# Only record transaction details for sells in the tax year
116116
if in_tax_year:
117117
matched_d = Decimal(str(matched_shares))
118-
cost_basis = float(matched_d * lot["cost_per_share"])
119-
proceeds = float(matched_d * proceeds_per_share)
120-
gain_loss = proceeds - cost_basis
118+
# Keep in Decimal for precise subtraction, convert to float at the end
119+
cost_basis_d = matched_d * lot["cost_per_share"]
120+
proceeds_d = matched_d * proceeds_per_share
121+
gain_loss_d = proceeds_d - cost_basis_d
122+
123+
cost_basis = float(cost_basis_d)
124+
proceeds = float(proceeds_d)
125+
gain_loss = float(gain_loss_d)
121126

122127
# Determine holding period
123128
holding_delta = trade.timestamp - lot["date"]
@@ -138,8 +143,6 @@ def calculate_capital_gains(
138143
if sell_fee > 0:
139144
tx["fee"] = sanitize_numeric(float(sell_fee))
140145
transactions.append(tx)
141-
142-
gain_loss_d = Decimal(str(gain_loss))
143146
if is_long_term:
144147
if gain_loss_d >= 0:
145148
long_term_gains += gain_loss_d

prediction_mcp/tools/chart_tools.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ..errors import safe_tool
2626
from ..serializers import to_json_text
2727
from ..validators import validate_chart_type, validate_market_slug
28+
from prediction_analyzer.exceptions import ChartError
2829

2930
logger = logging.getLogger(__name__)
3031

@@ -112,6 +113,12 @@ async def _handle_generate_chart(arguments: dict):
112113
raise ValueError("chart_type is required")
113114

114115
validate_chart_type(chart_type)
116+
if chart_type not in _CHART_GENERATORS:
117+
raise ChartError(
118+
f"Chart type '{chart_type}' is not supported for single-market charts. "
119+
f"Use 'generate_dashboard' for global charts, or choose from: "
120+
f"{sorted(_CHART_GENERATORS.keys())}"
121+
)
115122
validate_market_slug(market_slug, get_unique_markets(session.trades))
116123

117124
trades = filter_trades_by_market_slug(session.trades, market_slug)

prediction_mcp/validators.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,21 @@ def validate_export_format(fmt: str) -> str:
8787

8888

8989
def validate_positive_int(value: Optional[int], param_name: str) -> Optional[int]:
90-
"""Validate that an integer parameter is positive."""
90+
"""Validate that an integer parameter is positive.
91+
92+
Accepts float-typed integers (e.g. 5.0) since JSON doesn't
93+
distinguish between 5 and 5.0, and LLMs commonly send these.
94+
"""
9195
if value is None:
9296
return None
9397
if isinstance(value, float):
9498
if math.isnan(value) or math.isinf(value):
9599
raise InvalidFilterError(
96100
f"Invalid {param_name}: {value}. Must be a positive integer, not NaN/Infinity."
97101
)
102+
# Accept integral floats like 5.0 by converting to int
103+
if value == int(value):
104+
value = int(value)
98105
if not isinstance(value, int) or value < 1:
99106
raise InvalidFilterError(f"Invalid {param_name}: {value}. Must be a positive integer.")
100107
return value

0 commit comments

Comments
 (0)