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.
- bitsentry/__init__.py +18 -0
- bitsentry/api/__init__.py +0 -0
- bitsentry/api/server.py +273 -0
- bitsentry/audit_engine.py +329 -0
- bitsentry/bgc_client.py +153 -0
- bitsentry/cli.py +10 -0
- bitsentry/mcp/__init__.py +3 -0
- bitsentry/mcp/server.py +169 -0
- bitsentry/position_monitor.py +223 -0
- bitsentry/reporter.py +227 -0
- bitsentry/risk_guardian.py +256 -0
- bitsentry/scheduler.py +81 -0
- bitsentry/strategy_evaluator.py +269 -0
- bitsentry-0.1.0.dist-info/METADATA +69 -0
- bitsentry-0.1.0.dist-info/RECORD +17 -0
- bitsentry-0.1.0.dist-info/WHEEL +4 -0
- bitsentry-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -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,,
|