TRISHULA has been live-trading on Binance Futures USDC testnet for 14 days (April 10–24, 2026). The strategy deploys a market-neutral long/short portfolio driven by a 3-pronged ensemble signal (LSTM + TCN + LightGBM). Over this period, the portfolio generated a net loss of −$39.81 (−0.80%) on ~$5,000 capital, while Bitcoin gained +7.25% — an underperformance of −8.05% vs the simplest benchmark.
The short leg is the entire drag. Longs generated +$106.69 while shorts lost −$146.50. The strategy would have been profitable as a long-only system. Short signals have a 42% win rate with an asymmetric risk profile: avg loss (−5.1%) is nearly 2x avg win (+2.6%).
Of 166 closed trades, 41 were noise (account resets, migrations, emergency closes at $0 PnL). Only 125 trades had meaningful P&L. This inflates the loss count and suppresses the true win rate. Excluding flat trades: Long win rate = 58.9% (43/73), Short win rate = 42.3% (22/52).
| Close Reason | Count | Avg Return | Total P&L | Verdict |
|---|---|---|---|---|
| Take Profit (+4.5%) | 24 | +6.3% | +$154.42 | Working well |
| Stop Loss (−8%) | 9 | −9.5% | −$82.52 | Working, but hit too often on shorts |
| Expiry (3-day) | 32 | −1.4% | −$59.82 | Signals decaying before expiry |
| Exchange Reset | 24 | −1.5% | −$50.51 | Testnet noise |
| Emergency/Migration | 48 | −0.1% | −$10.52 | Operational noise |
| Rebalance | 9 | +0.08% | +$1.17 | Neutral |
Daily P&L shows high volatility with two large drawdowns (Apr 14: −$57.25 cumulative, Apr 21: −$33.87). The portfolio briefly went positive on Apr 20 (+$17.95) before a sharp −$51.82 loss wiped the gains.
Max drawdown: −$57.25 (−1.15% of capital) on April 14. The largest single-day loss was −$51.82 on April 21. Recovery factor: 0.31x (net P&L / max drawdown).
The strategy is designed to be market-neutral — deploying roughly equal capital to longs and shorts. However, the two legs have diverged sharply in performance, revealing a fundamental asymmetry in the signal model's predictive power.
The model's "most bearish" signals (lowest signal_score) should predict price declines. Instead, many shorted coins rallied — the average short loses −5.11% when it loses, nearly 2x the average win of +2.62%. This asymmetry suggests the short signal is capturing momentum (coins dropping hard then bouncing) rather than future weakness.
If TRISHULA ran as a long-only system (no shorts), deploying the same $2,500 per side into longs only:
| Coin | Trades | Total P&L | Avg P&L% | Win Rate | Capital |
|---|---|---|---|---|---|
| ZEC (Zcash) | 11 | +$28.18 | +1.59% | 45% (5/11) | $2,331.68 |
| ARB (Arbitrum) | 8 | +$26.50 | +2.12% | 50% (4/8) | $1,774.87 |
| SUI | 8 | +$24.87 | +1.96% | 63% (5/8) | $1,778.00 |
| LINK (Chainlink) | 8 | +$22.81 | +1.80% | 75% (6/8) | $1,774.21 |
| XRP | 7 | +$11.64 | +1.14% | 29% (2/7) | $1,934.56 |
| Coin | Trades | Total P&L | Avg P&L% | Win Rate | Capital |
|---|---|---|---|---|---|
| AAVE | 9 | −$52.70 | −3.52% | 22% (2/9) | $1,630.49 |
| ENA (Ethena) | 6 | −$49.19 | −5.36% | 33% (2/6) | $887.82 |
| UNI (Uniswap) | 8 | −$30.18 | −2.54% | 13% (1/8) | $1,813.61 |
| TIA (Celestia) | 7 | −$27.26 | −2.30% | 14% (1/7) | $1,061.81 |
| FIL (Filecoin) | 8 | −$16.82 | −1.41% | 13% (1/8) | $1,823.34 |
The 5 worst performers (AAVE, ENA, UNI, TIA, FIL) have combined 24 short trades that account for most of the losses. These are high-beta altcoins that rallied during the BTC uptrend. The model shorted them because of negative signal scores, but in a risk_on regime, negative-score altcoins still rally with the market.
The ensemble model produces a signal_score for each coin. Positive scores predict outperformance (→ long), negative scores predict underperformance (→ short). The critical question: does signal_score actually predict returns?
| Score Range | Direction | Trades | Avg P&L% | Win Rate | Verdict |
|---|---|---|---|---|---|
| > 0.05 | LONG | 4 | +2.42% | 75% | Strong signal |
| 0.01 to 0.05 | LONG | 5 | +0.69% | 60% | Decent |
| −0.02 to 0.01 | LONG | 14 | +1.74% | 43% | Mixed — high variance |
| −0.03 to −0.02 | LONG | 32 | +1.10% | 50% | Bulk of longs, slightly positive |
| −0.05 to −0.03 | SHORT | 30 | −1.79% | 33% | Signal failing |
| < −0.05 | SHORT | 33 | −0.16% | 39% | Flat — no edge |
Long signals show weak but positive correlation: higher scores → better returns. But short signals show no correlation — the most negative scores (−0.05 to −0.14) perform almost the same as mildly negative scores (−0.03 to −0.05). The model has predictive power for "what will go up" but not "what will go down."
Nearly all signals cluster in a narrow band: −0.05 to +0.05. The model rarely produces high-conviction signals. This means the portfolio is mostly filled with low-confidence positions, diluting any edge. The few high-confidence long trades (score > 0.05) actually perform well.
| Parameter | Value | Assessment |
|---|---|---|
| Max Positions per Side | 15 | Appropriate for diversification |
| Target Deploy % | 95% | Aggressive — leaves no dry powder |
| Hold Period | 3 days | Matches label_3d signal |
| Stop Loss | −8% | Reasonable for 3-day hold |
| Take Profit | +4.5% | Asymmetric: TP < SL → cuts winners short |
| Min Signal (Long) | > −0.15 | Too wide — includes low-conviction entries |
| Max Signal (Short) | < 0.00 | Too permissive — any negative signal triggers short |
TP at +4.5% vs SL at −8% creates a negative expected value structure. Take-profit fires on winners at +4.5%, but losers can bleed to −8% before being stopped. This is backwards: winners should be allowed to run further than losers. Recommended: TP ≥ SL (e.g., TP=8%, SL=5%) or use trailing stops.
| Coin | Dir | Entry | Size (USDC) | Score | Opened |
|---|---|---|---|---|---|
| ZEC | LONG | $356.47 | $161.84 | +0.103 | Apr 24 12:00 |
| ARB | LONG | $0.1303 | $162.23 | +0.046 | Apr 24 12:00 |
| SOL | LONG | $86.16 | $161.12 | +0.044 | Apr 24 12:00 |
| DOGE | LONG | $0.0982 | $161.59 | +0.038 | Apr 24 12:00 |
| TIA | LONG | $0.3555 | $147.89 | +0.035 | Apr 23 04:00 |
| LTC | LONG | $55.44 | $143.42 | +0.019 | Apr 21 12:00 |
| AAVE | LONG | $93.47 | $140.20 | +0.019 | Apr 24 04:00 |
| LINK | LONG | $9.284 | $140.37 | +0.015 | Apr 24 04:00 |
| ETH | LONG | $2,329.42 | $160.73 | +0.012 | Apr 24 12:00 |
| XRP | LONG | $1.4385 | $161.69 | +0.005 | Apr 24 12:00 |
| BNB | LONG | $638.66 | $159.67 | −0.002 | Apr 24 12:00 |
| ENA | LONG | $0.1101 | $180.64 | −0.006 | Apr 24 00:00 |
| SUI | LONG | $0.9691 | $167.27 | −0.011 | Apr 22 08:00 |
| CRV | LONG | $0.2205 | $157.24 | −0.011 | Apr 23 20:00 |
| ADA | LONG | $0.251 | $161.74 | −0.012 | Apr 24 12:00 |
| BTC | SHORT | $78,237.90 | $156.48 | −0.014 | Apr 24 12:00 |
| NEO | SHORT | $2.878 | $156.62 | −0.029 | Apr 24 12:00 |
| UNI | SHORT | $3.257 | $156.34 | −0.030 | Apr 24 12:00 |
| BCH | SHORT | $457.36 | $156.87 | −0.030 | Apr 24 12:00 |
| FIL | SHORT | $0.948 | $166.37 | −0.044 | Apr 22 08:00 |
In-depth analysis of every function in spot_bot.py (561 lines) and futures_exchange.py (272 lines), identifying bugs, risks, and optimization opportunities.
total_equity calculated in long block is used in short block (line 360) without recalculation — if a long trade depletes balance, short sizing uses stale equity.open_positions fetched at line 295 is stale — doesn't include positions just opened in the same cycle. The slug dedup via open_slugs dict works, but size calculations may be off.DATE(timestamp) which means signals from any hour on the latest day are used. If hourly signals run at :00 and the bot runs at :30, it correctly gets the latest batch. But if the ensemble fails silently, yesterday's stale signals are used with no warning.MIN_SIGNAL_SCORE = -0.15 for longs is too permissive — most longs have scores between −0.03 and 0.00, meaning the filter barely removes anything.set_sandbox_mode(True) wrapped in try/except (line 76) — works with pinned ccxt 4.3.85 but will break silently if upgraded. URL override on line 80 is the real testnet config.open_short parameter is named usdt_amount (line 183) — should be usdc_amount for consistency with the USDC migration. Cosmetic but confusing.usdt_free and usdt_total (line 247-248) even though fetching USDC balance. Should be renamed to usdc_free/usdc_total to avoid confusion.build_exchange() (line 454) and get_price()/get_balance() — these functions don't exist in the current codebase. This function is broken and will crash on execution. Likely a leftover from the spot trading era.14-day performance on $5,000 testnet capital
Set SHORT_N = 0 or MAX_OPEN_POSITIONS = 0 for shorts. This alone would have turned −$39.81 into +$106.69. The model doesn't predict downside — don't force it to.
Train a dedicated short model with different features: volume decline, on-chain outflows, funding rate, open interest decay. The current signal_score is optimized for long prediction and inverted for shorts — that doesn't work.
| Current | Recommended | Rationale |
|---|---|---|
| TP: +4.5% | TP: +8% or trailing | Let winners run to match SL magnitude |
| SL: −8% | SL: −5% | Cut losers faster, reduce avg loss |
| Hold: 3 days fixed | Hold: 3-5 days adaptive | Extend if in profit, cut if flat |
| Parameter | Current | Recommended |
|---|---|---|
| MIN_SIGNAL_SCORE (Long) | −0.15 | −0.03 or 0.00 |
| MAX_SIGNAL_SCORE (Short) | 0.00 | −0.05 |
| LONG_N | 15 | 10 (concentrate on highest conviction) |
| SHORT_N | 15 | 5 or 0 (reduce exposure) |
| Bug | File:Line | Impact |
|---|---|---|
show_status() calls undefined build_exchange() | spot_bot.py:454 | --status flag crashes |
usdt_amount parameter name in open_short() | futures_exchange.py:183 | Cosmetic confusion |
Balance keys usdt_free/usdt_total | futures_exchange.py:247 | Naming inconsistency with USDC |
| Stale signal detection missing | spot_bot.py:116 | Trades on old signals if ensemble fails |
Long signals are profitable. The ensemble has genuine predictive power for upside: +$106.69 over 14 days, 58.9% win rate (excl flat), average win (+3.73%) exceeds average loss (−2.84%). ZEC, ARB, SUI, and LINK consistently profit. The signal model is valid — the deployment strategy needs fixing.