bitsentry 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,269 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timedelta, timezone
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from bitsentry.audit_engine import AuditEngine
10
+
11
+
12
+ @dataclass
13
+ class StrategyHealth:
14
+ strategy_tag: str
15
+ total_trades: int
16
+ win_rate_7d: float
17
+ win_rate_30d: float
18
+ total_pnl_usdt: float
19
+ avg_win_usdt: float
20
+ avg_loss_usdt: float
21
+ profit_factor: float
22
+ best_market_condition: str
23
+ worst_market_condition: str
24
+ verdict: str
25
+ recommendation: str
26
+
27
+
28
+ _VERDICTS = {
29
+ "PERFORMING": "Strategy is performing well. Continue running with current parameters.",
30
+ "DEGRADING": "Win rate is declining. Consider reducing position size by 50% and reviewing entry signals.",
31
+ "DEAD": "Strategy win rate is critically low. STOP trading this strategy immediately and backtest from scratch.",
32
+ "INSUFFICIENT_DATA": "Not enough trades to evaluate. Need at least 3 trades for a meaningful verdict.",
33
+ }
34
+
35
+
36
+ class StrategyEvaluator:
37
+ """
38
+ Tracks per-strategy trade results and produces health verdicts.
39
+
40
+ Data is stored in a `trade_results` table co-located in the
41
+ AuditEngine's SQLite database so everything stays in one file.
42
+ """
43
+
44
+ def __init__(self, audit_engine: "AuditEngine"):
45
+ self._audit = audit_engine
46
+ self._db_path = audit_engine.db_path
47
+ self._ensure_table()
48
+
49
+ # ── Schema ───────────────────────────────────────────────────────────────
50
+
51
+ def _ensure_table(self) -> None:
52
+ with self._conn() as conn:
53
+ conn.execute("""
54
+ CREATE TABLE IF NOT EXISTS trade_results (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ timestamp TEXT NOT NULL,
57
+ strategy_tag TEXT NOT NULL,
58
+ symbol TEXT NOT NULL,
59
+ side TEXT NOT NULL,
60
+ entry_price REAL NOT NULL,
61
+ exit_price REAL NOT NULL,
62
+ size_usdt REAL NOT NULL,
63
+ pnl_usdt REAL NOT NULL,
64
+ pnl_pct REAL NOT NULL,
65
+ outcome TEXT NOT NULL,
66
+ market_condition TEXT NOT NULL DEFAULT ''
67
+ )
68
+ """)
69
+
70
+ def _conn(self) -> sqlite3.Connection:
71
+ conn = sqlite3.connect(self._db_path)
72
+ conn.row_factory = sqlite3.Row
73
+ return conn
74
+
75
+ @staticmethod
76
+ def _now() -> str:
77
+ return datetime.now(timezone.utc).isoformat()
78
+
79
+ # ── PnL helpers ──────────────────────────────────────────────────────────
80
+
81
+ @staticmethod
82
+ def _calc_pnl(
83
+ side: str, entry_price: float, exit_price: float, size_usdt: float
84
+ ) -> tuple[float, float]:
85
+ """Return (pnl_usdt, pnl_pct) for a closed trade."""
86
+ if entry_price <= 0:
87
+ return 0.0, 0.0
88
+ if side.lower() in ("buy", "long"):
89
+ pnl_pct = (exit_price - entry_price) / entry_price
90
+ else:
91
+ pnl_pct = (entry_price - exit_price) / entry_price
92
+ pnl_usdt = pnl_pct * size_usdt
93
+ return round(pnl_usdt, 6), round(pnl_pct, 8)
94
+
95
+ # ── Write ────────────────────────────────────────────────────────────────
96
+
97
+ def record_trade_result(
98
+ self,
99
+ strategy_tag: str,
100
+ symbol: str,
101
+ side: str,
102
+ entry_price: float,
103
+ exit_price: float,
104
+ size_usdt: float,
105
+ market_condition: str = "",
106
+ ) -> int:
107
+ pnl_usdt, pnl_pct = self._calc_pnl(side, entry_price, exit_price, size_usdt)
108
+ outcome = "WIN" if pnl_usdt > 0 else "LOSS"
109
+
110
+ with self._conn() as conn:
111
+ cur = conn.execute(
112
+ """
113
+ INSERT INTO trade_results
114
+ (timestamp, strategy_tag, symbol, side, entry_price,
115
+ exit_price, size_usdt, pnl_usdt, pnl_pct, outcome, market_condition)
116
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
117
+ """,
118
+ (
119
+ self._now(), strategy_tag, symbol, side,
120
+ entry_price, exit_price, size_usdt,
121
+ pnl_usdt, pnl_pct, outcome, market_condition,
122
+ ),
123
+ )
124
+ return cur.lastrowid
125
+
126
+ # ── Evaluation helpers ───────────────────────────────────────────────────
127
+
128
+ @staticmethod
129
+ def _win_rate(rows: list[dict]) -> float:
130
+ if not rows:
131
+ return 0.0
132
+ wins = sum(1 for r in rows if r["outcome"] == "WIN")
133
+ return wins / len(rows)
134
+
135
+ @staticmethod
136
+ def _cutoff(days: int) -> str:
137
+ dt = datetime.now(timezone.utc) - timedelta(days=days)
138
+ return dt.isoformat()
139
+
140
+ def _rows_for(self, strategy_tag: str) -> list[dict]:
141
+ with self._conn() as conn:
142
+ return [
143
+ dict(r) for r in conn.execute(
144
+ "SELECT * FROM trade_results WHERE strategy_tag = ? ORDER BY id",
145
+ (strategy_tag,),
146
+ )
147
+ ]
148
+
149
+ def _rows_since(self, strategy_tag: str, days: int) -> list[dict]:
150
+ cutoff = self._cutoff(days)
151
+ with self._conn() as conn:
152
+ return [
153
+ dict(r) for r in conn.execute(
154
+ """SELECT * FROM trade_results
155
+ WHERE strategy_tag = ? AND timestamp >= ?
156
+ ORDER BY id""",
157
+ (strategy_tag, cutoff),
158
+ )
159
+ ]
160
+
161
+ @staticmethod
162
+ def _best_worst_condition(rows: list[dict]) -> tuple[str, str]:
163
+ """Return (best_condition, worst_condition) by win rate."""
164
+ conditions: dict[str, list[str]] = {}
165
+ for r in rows:
166
+ cond = r.get("market_condition") or "unknown"
167
+ conditions.setdefault(cond, []).append(r["outcome"])
168
+
169
+ if not conditions:
170
+ return "unknown", "unknown"
171
+
172
+ rates = {
173
+ cond: outcomes.count("WIN") / len(outcomes)
174
+ for cond, outcomes in conditions.items()
175
+ }
176
+ best = max(rates, key=lambda k: rates[k])
177
+ worst = min(rates, key=lambda k: rates[k])
178
+ return best, worst
179
+
180
+ @staticmethod
181
+ def _verdict(
182
+ total_trades: int,
183
+ win_rate_7d: float,
184
+ win_rate_30d: float,
185
+ profit_factor: float,
186
+ ) -> str:
187
+ if total_trades < 3:
188
+ return "INSUFFICIENT_DATA"
189
+ if win_rate_7d < 0.35:
190
+ return "DEAD"
191
+ if win_rate_30d >= 0.55 and profit_factor >= 1.2:
192
+ return "PERFORMING"
193
+ return "DEGRADING"
194
+
195
+ # ── Public API ───────────────────────────────────────────────────────────
196
+
197
+ def evaluate(self, strategy_tag: str) -> StrategyHealth:
198
+ all_rows = self._rows_for(strategy_tag)
199
+ rows_7d = self._rows_since(strategy_tag, 7)
200
+ rows_30d = self._rows_since(strategy_tag, 30)
201
+
202
+ total_trades = len(all_rows)
203
+ win_rate_7d = self._win_rate(rows_7d)
204
+ win_rate_30d = self._win_rate(rows_30d)
205
+
206
+ total_pnl = sum(r["pnl_usdt"] for r in all_rows)
207
+
208
+ wins = [r["pnl_usdt"] for r in all_rows if r["outcome"] == "WIN"]
209
+ losses = [r["pnl_usdt"] for r in all_rows if r["outcome"] == "LOSS"]
210
+
211
+ avg_win = (sum(wins) / len(wins)) if wins else 0.0
212
+ avg_loss = (sum(losses) / len(losses)) if losses else 0.0
213
+
214
+ total_wins = sum(wins)
215
+ total_losses = abs(sum(losses))
216
+ profit_factor = (total_wins / total_losses) if total_losses > 0 else 0.0
217
+
218
+ best_cond, worst_cond = self._best_worst_condition(all_rows)
219
+
220
+ v = self._verdict(total_trades, win_rate_7d, win_rate_30d, profit_factor)
221
+
222
+ # Log checkpoint to AuditEngine
223
+ self._audit.log_strategy_checkpoint(
224
+ strategy_tag=strategy_tag,
225
+ win_rate_7d=round(win_rate_7d, 4),
226
+ win_rate_30d=round(win_rate_30d, 4),
227
+ total_trades=total_trades,
228
+ verdict=v,
229
+ market_condition=best_cond,
230
+ )
231
+
232
+ return StrategyHealth(
233
+ strategy_tag=strategy_tag,
234
+ total_trades=total_trades,
235
+ win_rate_7d=round(win_rate_7d, 4),
236
+ win_rate_30d=round(win_rate_30d, 4),
237
+ total_pnl_usdt=round(total_pnl, 4),
238
+ avg_win_usdt=round(avg_win, 4),
239
+ avg_loss_usdt=round(avg_loss, 4),
240
+ profit_factor=round(profit_factor, 4),
241
+ best_market_condition=best_cond,
242
+ worst_market_condition=worst_cond,
243
+ verdict=v,
244
+ recommendation=_VERDICTS[v],
245
+ )
246
+
247
+ def evaluate_all(self) -> list[StrategyHealth]:
248
+ with self._conn() as conn:
249
+ tags = [
250
+ row[0] for row in conn.execute(
251
+ "SELECT DISTINCT strategy_tag FROM trade_results ORDER BY strategy_tag"
252
+ )
253
+ ]
254
+ return [self.evaluate(tag) for tag in tags]
255
+
256
+ def get_leaderboard(self) -> list[dict]:
257
+ healths = self.evaluate_all()
258
+ ranked = sorted(healths, key=lambda h: h.profit_factor, reverse=True)
259
+ return [
260
+ {
261
+ "strategy_tag": h.strategy_tag,
262
+ "profit_factor": h.profit_factor,
263
+ "win_rate_30d": h.win_rate_30d,
264
+ "total_pnl_usdt": h.total_pnl_usdt,
265
+ "total_trades": h.total_trades,
266
+ "verdict": h.verdict,
267
+ }
268
+ for h in ranked
269
+ ]
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: bitsentry
3
+ Version: 0.1.0
4
+ Summary: Safety, audit, and intelligence layer for Bitget trading agents and traders
5
+ Project-URL: Homepage, https://github.com/Benita2001/BitSentry
6
+ Project-URL: Repository, https://github.com/Benita2001/BitSentry
7
+ Author-email: Benita Chidera <0xbeni123@gmail.com>
8
+ License: MIT
9
+ Keywords: agent,audit,bitget,crypto,mcp,risk-management,trading
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Office/Business :: Financial :: Investment
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: fastapi>=0.104.0
17
+ Requires-Dist: httpx>=0.25.0
18
+ Requires-Dist: mcp>=1.0.0
19
+ Requires-Dist: pydantic>=2.0.0
20
+ Requires-Dist: python-dotenv>=1.0.0
21
+ Requires-Dist: pyyaml>=6.0.0
22
+ Requires-Dist: requests>=2.31.0
23
+ Requires-Dist: rich>=13.0.0
24
+ Requires-Dist: uvicorn>=0.24.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # BitSentry
28
+
29
+ Safety, audit, and intelligence layer for Bitget trading agents and traders.
30
+
31
+ ## Structure
32
+
33
+ - `bitsentry/bgc_client.py` — Bitget API client
34
+ - `bitsentry/audit_engine.py` — Trade audit and logging
35
+ - `bitsentry/risk_guardian.py` — Pre-trade risk enforcement
36
+ - `bitsentry/position_monitor.py` — Real-time position tracking
37
+ - `bitsentry/strategy_evaluator.py` — Strategy scoring and auditing
38
+ - `bitsentry/api/server.py` — FastAPI REST server (11 endpoints)
39
+ - `config/risk_rules.yaml` — Configurable risk parameters
40
+ - `run.py` — Server launcher with startup banner
41
+
42
+ ## Setup
43
+
44
+ ```bash
45
+ make install # pip install -e .
46
+ make run # start server on http://127.0.0.1:8000
47
+ make validate # generate audit report + HTML
48
+ make verify # verify SHA-256 integrity hash
49
+ ```
50
+
51
+ ## API Endpoints
52
+
53
+ | Method | Path | Description |
54
+ |--------|------|-------------|
55
+ | GET | `/health` | Server health + mode |
56
+ | GET | `/positions` | Open positions with safety ratings |
57
+ | GET | `/positions/summary` | GREEN/YELLOW/RED counts |
58
+ | GET | `/positions/safe-to-trade` | Pre-trade exposure check |
59
+ | POST | `/risk/check` | 5-layer risk middleware |
60
+ | GET | `/strategy/leaderboard` | Strategies ranked by profit factor |
61
+ | GET | `/strategy/{tag}` | Strategy health verdict |
62
+ | POST | `/strategy/record` | Record a trade result |
63
+ | GET | `/audit/report` | Full audit + SHA-256 hash |
64
+ | GET | `/audit/verify` | Verify integrity hash |
65
+ | GET | `/docs` | Swagger UI |
66
+
67
+ ## Stack
68
+
69
+ Python 3.10+, FastAPI, SQLite, bgc CLI, Pydantic v2
@@ -0,0 +1,17 @@
1
+ bitsentry/__init__.py,sha256=3gblFNTKgGR-pu66wAlWuMJukQYL0wYA8UFGa1OngZQ,704
2
+ bitsentry/audit_engine.py,sha256=YQirS8aNmMl7yJZgy0U3JC_nscTGeJwJejwarxTPG-c,12757
3
+ bitsentry/bgc_client.py,sha256=_EfCpnjAZCfhm2GZ79rtfEZ_xN8jaaTOE3p1_oC087s,5704
4
+ bitsentry/cli.py,sha256=J1ZN2sLdTl-Jb886I2R7p1to4EfvfnyyO6dduP8f6qY,288
5
+ bitsentry/position_monitor.py,sha256=-iGHGX4skiaR90RrQF5dLbyop1MlZHKOdjoiMOWWHkI,8252
6
+ bitsentry/reporter.py,sha256=dPeJWIur8-XVd0MR2bkZjRguVrxeyHB6tQZaHe_pmk0,8410
7
+ bitsentry/risk_guardian.py,sha256=UYYWR7QJjDcSfYaRocOApDn6l0Hl-Yl_ECQZBx2DEkw,9469
8
+ bitsentry/scheduler.py,sha256=SxX5lDEaOxvPwjoQ9k9u7YWypAelxLkSyBejads7Fz8,3137
9
+ bitsentry/strategy_evaluator.py,sha256=4m6vD3oVSHrpstBOYaCsBOSOZIaLbz8uZNV5ApyRZHM,10080
10
+ bitsentry/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ bitsentry/api/server.py,sha256=rIyvfyqwTRQjyMiikhqPsgL4F8h2udH9zs5l7JW_Mv4,9256
12
+ bitsentry/mcp/__init__.py,sha256=Hq-s7pLXZK_WbITMlnj_7Z8-fvJ-VNM8Fi-JPmJ_9gk,56
13
+ bitsentry/mcp/server.py,sha256=S-a6NcRwj_zK9JtEN3toQ_jy0Dsd_WZHNUr_ZNCYVho,6404
14
+ bitsentry-0.1.0.dist-info/METADATA,sha256=vqvtKuMTGUK0nTXw5XF_mofoWMnJF8NhWvT5sPnbPbc,2513
15
+ bitsentry-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
16
+ bitsentry-0.1.0.dist-info/entry_points.txt,sha256=aRCcTaeNFZvVdATFQncTacz3fcE3h7Xzoz36_UQI4yY,49
17
+ bitsentry-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bitsentry = bitsentry.cli:main