A practical handbook for getting the most out of Magellan's grid trading strategy on SOL/USDC.
This guide assumes you understand Solana, token swaps, and basic DeFi concepts. It focuses on how to think about the parameters and how to adapt to market conditions — not on installation or configuration syntax.
Appendices
Magellan is a scalping grid trader. It profits from the natural oscillation of SOL's price — the constant small ups and downs that happen even in a flat market.
The core cycle:
Price drops → Bot buys SOL cheap
Price recovers → Bot sells SOL at a small profit
Repeat
Each unit operates independently: buy low, sell high, recycle. A single trade might only make 0.2–0.5% profit, but across 10 units running 24/7, these small gains compound.
A day in the life of Unit 3 (with $50 per unit, TAKE_PROFIT=0.7, SOL at ~$150):
09:14 UTC — SOL drops to $148.50 → Unit 3's buy target hit → buys 0.3367 SOL for $50
09:14 UTC — Unit 3 is now HOLDING, watching for $149.54 (+0.7% above $148.50)
11:42 UTC — SOL recovers to $149.54 → sell triggers → sells 0.3367 SOL for $50.35
11:42 UTC — Unit 3 earns $0.35 gross, ~$0.15 in fees → $0.20 net profit
11:42 UTC — Unit 3 returns to IDLE → immediately gets a new buy target below market
→ Ready to do it all over again
One unit, one cycle, $0.20 profit. Multiply by 9 active units, several cycles per day, 365 days a year.
Key insight: Magellan doesn't need SOL to go up. It needs SOL to move. A sideways market with frequent 0.5–2% oscillations is ideal. A straight line (up or down) is the enemy.
SOL/USDC is one of the most liquid pairs on Solana with tight spreads on Jupiter. SOL typically oscillates 1–5% intraday even on "quiet" days. That's exactly the range Magellan exploits:
The grid is the backbone of the strategy. Think of it as a ladder of buy orders placed below the current price.
With SOL at $150 and these settings:
SPLIT_NUMBER=10
PRINCIPAL_LEFTOVER=10
GRID_SPREAD=0.5
The bot creates 9 active units (10 total minus 1 buffer) with staggered buy targets:
| Unit | Buy Target | Distance Below Market |
|---|---|---|
| 1 | $149.25 | -0.5% |
| 2 | $148.50 | -1.0% |
| 3 | $147.75 | -1.5% |
| 4 | $147.00 | -2.0% |
| 5 | $146.25 | -2.5% |
| 6 | $145.50 | -3.0% |
| 7 | $144.75 | -3.5% |
| 8 | $144.00 | -4.0% |
| 9 | $143.25 | -4.5% |
When price drops to a unit's target, that unit buys. When price recovers by TAKE_PROFIT% above its entry, it sells.
The grid center is the reference price from which all buy targets are calculated. It updates when:
Units already holding SOL are never affected by grid resets — only unfilled WAITING_TO_BUY orders get recalculated.
Example: grid center is $150, GRID_RESET_THRESHOLD=2. SOL rises to $154 (+2.7% drift):
Before reset: After reset:
Grid center: $150 Grid center: $154
Unit 1: WAITING at $149.25 (stale) → Unit 1: WAITING at $153.23
Unit 2: WAITING at $148.50 (stale) → Unit 2: WAITING at $152.46
Unit 3: HOLDING at $147.75 entry → Unit 3: HOLDING at $147.75 (unchanged!)
Unit 4: WAITING at $147.00 (stale) → Unit 4: WAITING at $151.69
Unit 3 was already holding SOL — it keeps its position and its original take-profit target. Only the unfilled WAITING units get fresh targets near the new price.
The deepest unit sits at active_units × GRID_SPREAD below the current price (since the first unit starts at 1 × GRID_SPREAD below market):
| GRID_SPREAD | Max Depth (9 units) | Behavior |
|---|---|---|
| 0.1% | 0.9% below market | Dense — many units trigger on small dips |
| 0.3% | 2.7% below market | Balanced — good for typical SOL volatility |
| 0.5% | 4.5% below market | Wide — catches deeper dips, fewer triggers |
| 1.0% | 9.0% below market | Very wide — only triggers on significant drops |
Narrow grids (0.1–0.2%) fire more often but risk having many units buy at nearly the same price. If the price keeps dropping, they all take losses together.
Wide grids (0.5–1.0%) catch different price levels, giving you better average entries during a dip. But in a tight oscillation range, only the top 1–2 units ever trigger.
Every trade has a cost. If your take-profit doesn't cover these costs, you lose money on winning trades.
In live mode, round-trip costs come from:
| Cost Component | Typical Range | Notes |
|---|---|---|
| DEX pool fees | 0.05–0.15% per side | Charged by underlying AMMs (Raydium, Orca); Jupiter routes through them |
| Slippage | 0.01–0.10% per side | Depends on trade size and pool depth |
| Solana tx fee | ~0.000005 SOL | Negligible (~$0.001) |
| Priority fees | 0–0.0001 SOL | Only during congestion; set PRIORITY_FEE=Y and PRIORITY_FEE_LAMPORTS in config.env, or leave PRIORITY_FEE=N for Jupiter auto-estimation |
Typical round-trip cost in live mode: 0.1–0.5%
This means:
TAKE_PROFIT=0.7 → net profit ~0.2–0.6% per trade ✔TAKE_PROFIT=0.5 → net profit ~0.0–0.4% per trade ⚠ (marginal)TAKE_PROFIT=0.3 → likely break-even or negative ❌Paper mode simulates costs with SLIPPAGE_BPS and PAPER_DEX_FEE:
Round-trip cost = (SLIPPAGE_BPS / 2 / 10000 + PAPER_DEX_FEE) × 2
With the code defaults (SLIPPAGE_BPS=50, PAPER_DEX_FEE=0.0025): ~1.0% round-trip — deliberately pessimistic so paper profits don't give false confidence.
For faster paper testing with more trade activity, lower these values (e.g., SLIPPAGE_BPS=10, PAPER_DEX_FEE=0.0005 → ~0.2% round-trip). When transitioning to live, keep your TAKE_PROFIT well above real costs.
Net profit per trade = TAKE_PROFIT% - round-trip cost%
Example with $50 unit size in live mode:
TAKE_PROFIT=0.7%, round-trip cost ~0.3%Same setup with TAKE_PROFIT=0.3%:
Rule of thumb for live trading: set TAKE_PROFIT to at least 2× your expected round-trip cost. If you estimate ~0.3% round-trip costs, use TAKE_PROFIT=0.7 (2.3× costs). This gives you a safety margin for slippage spikes.
These four parameters determine how and when the bot trades.
This is the percentage gain above entry price that triggers a sell.
TAKE_PROFIT=0.7 # Sell when +0.7% above buy price
Higher take-profit (0.7–1.0%):
Lower take-profit (0.3–0.5%):
Example: SOL at $150 with TAKE_PROFIT=0.7
Example: Same setup with TAKE_PROFIT=0.3
This is the percentage drop below entry price that triggers a forced sell.
STOP_LOSS=2 # Cut losses at -2% below buy price
Stop-loss exists to free up capital. Without it, a unit that bought during a downturn sits idle holding a losing position forever, unable to participate in new trades.
Tight stop-loss (1–1.5%):
Wide stop-loss (2–3%):
Example — tight vs. wide stop-loss in the same scenario:
Unit buys at $150. SOL drops to $147 (-2%), then recovers to $152 (+1.3%):
| Setting | What happens | Result |
|---|---|---|
STOP_LOSS=1.5 | Stop-loss triggers at $147.75 → forced sell → unit misses the recovery | -$0.75 loss (1.5% of $50 unit) |
STOP_LOSS=2 | Stop-loss triggers at $147.00 → forced sell → unit misses the recovery | -$1.00 loss (2% of $50 unit) |
STOP_LOSS=3 | Price never hits $145.50 → unit holds through the dip → sells at $151.05 (+0.7%) | +$0.35 profit |
The wide stop-loss won here because the dip reversed. But if SOL had kept falling to $140, the wide stop-loss would have lost $1.50 (3%) instead of $0.75 (1.5%).
The stop-loss / take-profit ratio matters — and costs make it worse. Trading costs (DEX fees + slippage) reduce your net profit on wins but increase your actual loss on stop-outs (you pay fees on the losing sell too). In live mode, with ~0.3% round-trip costs:
| TAKE_PROFIT | STOP_LOSS | Net Win | Actual Loss | Wins to Recover | Min Win Rate |
|---|---|---|---|---|---|
| 0.7% | 2% | ~0.4% | ~2.3% | ~6 wins | >85% |
| 0.7% | 3% | ~0.4% | ~3.3% | ~8 wins | >89% |
| 0.5% | 2% | ~0.2% | ~2.3% | ~12 wins | >92% |
| 0.5% | 1.5% | ~0.2% | ~1.8% | ~9 wins | >90% |
These numbers look demanding, but grid trading in oscillating markets naturally produces very high win rates (often 90–95%) because most buy-the-dip entries recover. The key is ensuring the rare stop-loss events don't wipe out accumulated gains.
The practical takeaway: wider TAKE_PROFIT (0.7%+) and moderate STOP_LOSS (2%) give you the best cushion. Avoid thin take-profits with wide stop-losses — the math is brutal.
GRID_SPREAD=0.3 # 0.3% between each unit's buy target
This controls how deep your grid extends below the current price. The deepest unit sits at GRID_SPREAD × active_units below market (since the first unit starts at 1 × GRID_SPREAD).
With 9 active units and GRID_SPREAD=0.3, your deepest unit is 2.7% below the current price.
Key trade-off: wider spread = better diversified entries but fewer triggers.
A good starting rule: set GRID_SPREAD roughly equal to TAKE_PROFIT / 2. This way, if price drops enough to trigger 2-3 units, it only needs to recover by TAKE_PROFIT% for all of them to profit.
Example: TAKE_PROFIT=0.7, GRID_SPREAD=0.35, SOL at $150:
If GRID_SPREAD were too wide (e.g., 1%), Unit 2 would be at $148.50 and Unit 3 at $147 — most units would never trigger in a small dip, leaving them idle.
GRID_RESET_THRESHOLD=2 # Reset grid if price drifts >2% from center
When the market moves significantly in one direction, your grid of unfilled buy orders becomes stale — they're all too far from the current price to ever trigger. The grid reset cancels them and rebuilds around the new price.
Low threshold (1–1.5%): resets frequently, keeps grid close to market. Risk: grid "chases" the price during trends, buying at each new level during a sustained drop.
High threshold (2–3%): only resets on significant moves. More stable, but if price slowly drifts away, the grid sits idle for longer.
Example — low vs. high threshold during a $150 → $155 trend:
| Threshold | Resets at | What happens |
|---|---|---|
| 1.5% | $152.25 (+1.5%) | Grid rebuilds at $152 — then again at $154.28 — constant chasing. If it's a downtrend disguised as noise, units keep buying higher and higher |
| 3% | $154.50 (+3%) | Grid stays anchored at $150 until +3% drift. Fewer resets, but unfilled buy orders may sit stale for hours until the reset finally fires |
In a genuine uptrend, low threshold = more opportunities. In a fakeout, low threshold = more exposure. There's no perfect answer — match it to your risk tolerance.
Rule of thumb: set GRID_RESET_THRESHOLD to approximately your grid's max depth. If your deepest unit sits at ~2.7% below market (9 units × 0.3% spread), a reset threshold of 2–3% ensures the grid rebuilds before it becomes completely out of range.
MAX_DAILY_LOSS=5 # Halt if daily losses exceed 5% of PRINCIPAL
With PRINCIPAL=500, this means trading halts if you lose $25 in a single day.
When triggered, the bot enters sell-only mode: it still executes stop-loss sells on holding positions (to limit further damage) but doesn't open any new buys. Resets at midnight UTC.
| Level | MAX_DAILY_LOSS | With $500 principal | With $5,000 principal |
|---|---|---|---|
| Conservative | 3% | Halts after -$15 | Halts after -$150 |
| Moderate | 5% | Halts after -$25 | Halts after -$250 |
| Aggressive | 10% | Halts after -$50 | Halts after -$500 |
Example: you're running with PRINCIPAL=500, SPLIT_NUMBER=10, STOP_LOSS=3, MAX_DAILY_LOSS=3, and SOL flash-crashes -8%. Three units get stopped out at -3% each, losing $1.50 × 3 = $4.50. Two more units stop out: total = $7.50. By the time a sixth unit stops out, daily loss reaches $9.00. Three more stop-losses and you hit -$13.50 (all 9 active units). The grid resets at the new lower price — the next stop-loss pushes you past -$15 → bot halts, preventing further damage while the crash plays out. Without the limit, the grid would keep resetting and buying into the continuing crash, snowballing losses far beyond $13.50.
PRINCIPAL_LEFTOVER=10 # Keep 10% of principal in reserve
This reserves units as an idle buffer. With 10 units and 10% leftover, 9 units trade and 1 stays idle.
Why keep a buffer?
Example: PRINCIPAL=500, SPLIT_NUMBER=10, PRINCIPAL_LEFTOVER=10:
PRINCIPAL_LEFTOVER=0: all 10 units would have been stopped out, losing 10 × $1.00 = $10.00, and no buffer remainsFor live trading, 10% is a solid default. Drop to 5% once you're comfortable with the bot's behavior. Only set 0% if you fully trust the parameters and understand you're maximizing exposure.
HAPPY_GAIN=3 # Transfer profits when cumulative gains reach 3 USDC
When cumulative profit reaches this threshold, the bot transfers exactly HAPPY_GAIN USDC to your BENEFICIARY_WALLET. Any excess carries over (e.g., if cumulative is $3.40 and HAPPY_GAIN is $3, it sends $3.00 and keeps $0.40 as the new cumulative balance). This is your profit extraction mechanism — it moves gains to a separate wallet so they're no longer at risk.
Frequent transfers (low HAPPY_GAIN, e.g. 2–3 USDC): safer, you realize gains often, but more transfer transactions.
Infrequent transfers (high HAPPY_GAIN, e.g. 10–20 USDC): fewer transactions, but more unrealized profit sitting in the operator wallet at risk.
Tip: set HAPPY_GAIN relative to your daily expected profit. If you expect ~$5/day, HAPPY_GAIN=3 transfers roughly twice daily.
Magellan works at any scale, but there's a practical minimum:
| PRINCIPAL | SPLIT_NUMBER | Unit Size | Viability |
|---|---|---|---|
| $50 | 5 | $10 | Minimum viable — small trades, limited grid depth |
| $100 | 10 | $10 | Good starting point for paper testing |
| $500 | 10 | $50 | Solid for live trading — meaningful per-trade profit |
| $1,000 | 15 | ~$67 | Good density — 15-level grid catches more oscillations |
| $5,000 | 20 | $250 | Deep grid with significant per-trade returns |
More units = denser grid = more buy levels. But there are diminishing returns:
MIN_TRADE_USDC=1 # Skip trades below 1 USDC
Jupiter has a minimum swap of ~$0.15, but you should set this higher. Tiny trades generate more fees relative to their size. With a unit size of $50, set MIN_TRADE_USDC=1 (or even $5). The goal is to avoid dust-sized trades that waste gas.
SOL bouncing between $148–$152 with no clear trend.
TAKE_PROFIT=0.5
STOP_LOSS=2
GRID_SPREAD=0.2
GRID_RESET_THRESHOLD=2
Why: tight take-profit catches frequent oscillations. Grid spread is narrow because price isn't moving far. Stop-loss is wide enough to avoid false triggers.
Expected behavior: many units cycle through buy→sell within hours. High trade volume, consistent small profits.
Play-by-play ($500 principal, 9 active units, $50/unit):
08:00 SOL $150.00 — Grid placed. Unit 1 target: $149.70, Unit 2: $149.40...
08:45 SOL $149.60 — Units 1-2 buy
09:20 SOL $150.10 — Units 1-2 sell (+0.5% each) → +$0.50 profit
10:30 SOL $149.40 — Units 1-3 buy (grid re-placed after recycle)
11:15 SOL $150.20 — All 3 sell → +$0.75 profit
14:00 SOL $148.80 — Units 1-5 buy (deeper dip)
15:45 SOL $149.55 — Units 1-5 sell → +$1.25 profit
Daily total: 10 round-trips, ~$2.50 profit, zero stop-losses
SOL swinging 3–8% intraday.
TAKE_PROFIT=0.7
STOP_LOSS=3
GRID_SPREAD=0.5
GRID_RESET_THRESHOLD=3
Why: wider take-profit and grid spread match the larger swings. Higher stop-loss gives positions room to survive temporary dips before recovering.
Expected behavior: fewer but larger profits. Some units may hold for hours waiting for the recovery swing. Occasional stop-losses during sharp drops, but winning trades more than compensate.
SOL climbing steadily from $140 to $160 over a few days.
TAKE_PROFIT=0.7
STOP_LOSS=2
GRID_SPREAD=0.3
GRID_RESET_THRESHOLD=1.5
Why: lower grid reset threshold keeps the grid chasing the price upward. Units buy on pullbacks and sell on the next leg up. Works well as long as the trend has regular pullbacks.
Expected behavior: grid resets frequently as price climbs. Each reset anchors new buy targets just below the new price. Units cycle on pullback oscillations within the trend.
Warning: a clean uptrend with no pullbacks means no buy triggers. The bot sits idle waiting for a dip. That's fine — it means you're not buying into an overextended move.
SOL falling steadily from $160 to $140.
This is the hardest market for grid trading. Units buy on what looks like a dip, but the price keeps falling, triggering stop-losses.
TAKE_PROFIT=0.7
STOP_LOSS=1.5
GRID_SPREAD=0.5
GRID_RESET_THRESHOLD=3
MAX_DAILY_LOSS=3
Why: tighter stop-loss limits damage per position. Wider grid spread means units are spread across a broader range (not all buying near the top of the drop). Low daily loss cap halts trading early. High reset threshold prevents the grid from chasing the price down.
Expected behavior: some stop-losses triggered. Daily loss limit may halt trading — and that's a good thing. The bot protects capital and waits for conditions to improve.
Play-by-play ($500 principal, 9 active units, $50/unit):
08:00 SOL $160 — Grid placed. Units 1-9 waiting at $159.20 to $156.00
09:30 SOL $158 — Units 1-4 buy (targets hit)
10:15 SOL $156 — Units 5-7 buy, Units 1-4 still holding (waiting for recovery)
11:00 SOL $155 — Units 1-4 hit stop-loss at -1.5% → sell → -$3.00 total loss
11:00 SOL $155 — Units 5-7 now holding, waiting for +0.7% recovery
12:30 SOL $153 — Units 5-7 hit stop-loss → sell → -$2.25 total loss
12:30 Daily P&L: -$5.25 → still under $15 limit (3% of $500)
14:00 SOL $151 — Grid resets. New units buy at $150.24 to $147.50
15:00 SOL $149 — New units hit stop-loss → -$3.38 loss
15:00 Daily P&L: -$8.63 → still under $15 limit
16:00 SOL $147 — More stop-losses → daily loss reaches -$15 → BOT HALTS
Bot saved you from the continued slide to $140 by end of day
SOL stuck at $150.00 ± $0.20 for days.
Grid trading doesn't work here. There's not enough movement to trigger buys, or if units buy, the price doesn't move enough to hit take-profit.
Best approach: either leave the bot running with Level 1 (Conservative) settings and accept low activity, or pause it and wait for volatility to return. Don't lower TAKE_PROFIT below your cost threshold just to get trades — you'll lose on every cycle.
You want to trade real capital with minimal risk.
MODE=live
PRINCIPAL=500
SPLIT_NUMBER=10
PRINCIPAL_LEFTOVER=10
TAKE_PROFIT=0.7
STOP_LOSS=3
GRID_SPREAD=0.5
GRID_RESET_THRESHOLD=3
MAX_DAILY_LOSS=3
HAPPY_GAIN=5
SLIPPAGE_BPS=50
Breakdown:
With SOL oscillating ~2% daily, you might see 5–15 completed trades per day, mostly winners. A realistic daily return: $1–3 (0.2–0.6% of principal).
You want to see lots of trades and understand the bot's behavior.
MODE=paper
PRINCIPAL=100
SPLIT_NUMBER=10
PRINCIPAL_LEFTOVER=10
TAKE_PROFIT=0.3
STOP_LOSS=1.5
GRID_SPREAD=0.2
GRID_RESET_THRESHOLD=1.5
MAX_DAILY_LOSS=7
SLIPPAGE_BPS=10
PAPER_DEX_FEE=0.0005
Breakdown:
Use the dashboard's History tab to study patterns: when do stop-losses cluster? What time of day generates the most wins? How often does the grid reset?
You've validated the strategy in paper mode and small live, now deploying serious capital.
MODE=live
PRINCIPAL=5000
SPLIT_NUMBER=20
PRINCIPAL_LEFTOVER=10
TAKE_PROFIT=0.7
STOP_LOSS=2
GRID_SPREAD=0.3
GRID_RESET_THRESHOLD=2
MAX_DAILY_LOSS=3
HAPPY_GAIN=20
SLIPPAGE_BPS=50
Breakdown:
With 18 active units and a deep 5.4% grid, you catch a wide range of dips. Realistic daily return: $10–30 (0.2–0.6% of principal). Profits auto-transfer every time $20 accumulates.
Don't go straight from zero to live trading. Follow this pipeline:
MODE=paper
TAKE_PROFIT=0.3
GRID_SPREAD=0.2
Goal: generate lots of trades to understand how the grid behaves. Watch the dashboard. Look at:
MODE=paper
TAKE_PROFIT=0.7
GRID_SPREAD=0.5
Goal: validate that your actual configuration is profitable. Collect enough data for statistical significance (at least 20–30 completed trades). Check the History tab:
MODE=live
PRINCIPAL=50
Goal: verify real execution. Jupiter swaps, actual slippage, real gas costs. Compare live P&L to paper P&L. Live will typically be slightly worse due to real-world execution costs.
Once live results match or are close to paper results over 1–2 weeks, gradually increase PRINCIPAL. Don't jump from $50 to $5,000. Go $50 → $200 → $500 → $1,000 → target.
If TAKE_PROFIT doesn't exceed your round-trip trading costs, every "winning" trade actually loses money. In live mode, keep it at 0.7% minimum unless you've verified lower values are profitable with your specific RPC/routing setup.
Example: you set TAKE_PROFIT=0.3 with a $50 unit. The bot buys at $149.25, sells at $149.70 — gross profit $0.15. But Jupiter fees + slippage cost $0.15 on the buy and $0.10 on the sell = $0.25 total fees. Net result: -$0.10 loss on a "winning" trade. Repeat 20 times a day and you lose $2/day while the dashboard shows positive trades.
A 0.5% take-profit with a 3% stop-loss sounds like 6:1, but costs make it far worse. After ~0.3% round-trip costs, your net win is ~0.2% while your actual loss is ~3.15% (stop-loss + sell fees). That's ~16 wins needed per loss. If your win rate drops below 94%, you bleed capital. Keep the ratio manageable — ideally STOP_LOSS should be no more than 3–4× your TAKE_PROFIT.
Example: with $50 units, TAKE_PROFIT=0.5 (net ~$0.10/win after fees) and STOP_LOSS=3 (loss = $1.58 with fees). You need 16 winning trades to recover from a single stop-loss. If you get 2 stop-losses in a row, that's 32 wins needed just to break even.
GRID_SPREAD=0.1 with 20 units means all 20 units buy within a 2% range below market. If SOL drops 2% and then drops another 3%, you have 20 units all underwater together. Wider spread = better risk distribution.
Example: SOL at $150, GRID_SPREAD=0.1, 20 units. All units have buy targets between $149.85 and $147.00. SOL drops to $147 → all 20 units buy. SOL continues to $143 → all 20 units hit stop-loss simultaneously. Loss: 20 × $1.50 = $30.00 in one move. Had you used GRID_SPREAD=0.5 (targets spanning $149.25 to $140.50), only the top 6–8 units would have triggered, limiting the damage.
When MAX_DAILY_LOSS triggers, it's doing its job. Don't increase it just because you hit it once. A day where you hit the loss limit is a day where the market conditions were bad for grid trading. The bot stopped you from losing more. That's a win.
Example: your bot hits MAX_DAILY_LOSS=3 ($15 loss on $500 principal) during a SOL selloff. You think "the selloff is almost over" and increase to MAX_DAILY_LOSS=10. SOL keeps dropping — the bot buys into every dead-cat bounce and stops out each time. End of day: -$42 loss instead of -$15. The original limit would have saved you $27.
Jupiter swaps involve multiple API calls (quote → swap → sign → send → confirm). If your RPC is slow or unreliable, transactions may timeout and retry, wasting gas and creating edge cases. Use a reliable RPC (Helius recommended) and enable FALLBACK_RPC with a second provider. During network congestion, if you see repeated TX_FAILED events in the logs, enable PRIORITY_FEE=Y with a higher PRIORITY_FEE_LAMPORTS value (e.g., 100000 for moderate congestion).
The dashboard exists for a reason. Check it daily. Look at:
GRID_RESET_THRESHOLD is too lowUse the Export CSV button in the History tab to download your trades. Open them in a spreadsheet and analyze: which times of day are most profitable? Do certain units perform better? Is there a pattern to stop-loss events? Data-driven tuning beats guesswork.
SOL volatility isn't uniform. US market hours (14:30–21:00 UTC) and Asian market hours (01:00–08:00 UTC) tend to be more volatile. You can:
This requires manually updating config.env and restarting, but can be scripted with a cron job that swaps config files.
Example setup with two config files:
# config.env.active — for volatile hours (US/Asia market overlap)
TAKE_PROFIT=0.5
GRID_SPREAD=0.2
# config.env.quiet — for low-volatility hours (overnight US)
TAKE_PROFIT=0.7
GRID_SPREAD=0.5
# Cron jobs (add via `crontab -e`):
# Switch to active at 14:30 UTC (US market open)
30 14 * * 1-5 cp config.env.active config.env && pm2 restart magellan
# Switch to quiet at 21:00 UTC (US market close)
0 21 * * 1-5 cp config.env.quiet config.env && pm2 restart magellan
Setting GRID_SPREAD=0 makes all units buy at the same price. This is essentially an all-in at a single level — useful if you're highly confident SOL will bounce from a specific support level.
GRID_SPREAD=0
TAKE_PROFIT=1.0
All 9 units buy together, and a 1% bounce gives you 9× the single-unit profit. But if the bounce doesn't come, 9 units all take the stop-loss together. High reward, high risk.
Set HAPPY_GAIN high enough that profits stay in the operator wallet and effectively increase your trading capital between transfers. The bot trades with PRINCIPAL, but if your wallet has more USDC than PRINCIPAL, the excess acts as additional buffer.
Example: PRINCIPAL=500, HAPPY_GAIN=50. The bot accumulates up to $50 in profit before transferring. That extra $50 in the wallet absorbs slippage and provides a larger cushion against stop-losses.
Export CSVs for multiple days and combine them in a spreadsheet. Create charts of:
This analysis tells you when your parameters need updating better than any single metric.
For serious live deployments, always enable fallback:
FALLBACK_ENABLED=Y
FALLBACK_RPC_URL=https://solana-mainnet.g.alchemy.com/v2/YOUR_KEY
If your primary RPC goes down, the bot automatically switches. Downtime = missed trades = missed profits.
TAKE_PROFIT > round-trip cost → profitability requirement
STOP_LOSS ≤ 3–4 × TAKE_PROFIT → manageable loss ratio
GRID_SPREAD ≈ TAKE_PROFIT / 2 → balanced grid density
GRID_RESET_THRESHOLD ≈ grid max depth → prevents stale grids
(grid depth = GRID_SPREAD × active_units)
MAX_DAILY_LOSS = your pain threshold → capital protection
| Condition | TAKE_PROFIT | STOP_LOSS | GRID_SPREAD | GRID_RESET |
|---|---|---|---|---|
| Choppy / sideways | 0.5% | 2% | 0.2% | 2% |
| Volatile swings | 0.7% | 3% | 0.5% | 3% |
| Trending up | 0.7% | 2% | 0.3% | 1.5% |
| Trending down | 0.7% | 1.5% | 0.5% | 3% |
| Low volatility | 0.7%+ | 3% | 0.5% | 3% |
MODE=live
TAKE_PROFIT=0.7 # covers live trading costs with margin
STOP_LOSS=2 # room to breathe, but not too much
GRID_SPREAD=0.3 # balanced density
GRID_RESET_THRESHOLD=2 # adapts to market movement
MAX_DAILY_LOSS=3 # conservative protection
PRINCIPAL_LEFTOVER=10 # safety buffer
HAPPY_GAIN=5 # regular profit extraction
SLIPPAGE_BPS=50 # standard Jupiter tolerance
Structural parameters define the shape of Magellan's trading grid and cannot be hot-reloaded while the bot is running. Changing them requires a full stop, state reset, and restart.
| Parameter | What It Controls | Default |
|---|---|---|
PRINCIPAL | Total USDC budget allocated to trading | 100 |
SPLIT_NUMBER | Number of trading units (grid slots) | 10 |
PRINCIPAL_LEFTOVER | % of principal reserved as safety buffer | 10 |
MODE | paper (simulated) or live (real swaps) | paper |
RPC_URL | Solana RPC endpoint | — |
FALLBACK_RPC_URL | Backup RPC endpoint | — |
JUPITER_API_URL | Jupiter swap API base URL | https://api.jup.ag/swap/v1 |
JUPITER_API_KEY | Jupiter API key | — |
OPERATOR_WALLET_PRIVATE_KEY | Base58 wallet private key | — |
BENEFICIARY_WALLET | Public key for profit transfers | — |
MIN_TRADE_USDC | Minimum trade size in USDC | 1 |
These are everything except the 9 hot-reloadable parameters (TAKE_PROFIT, STOP_LOSS, GRID_SPREAD, GRID_RESET_THRESHOLD, MAX_DAILY_LOSS, SOL_GAS_AMOUNT, SLIPPAGE_BPS, HAPPY_GAIN, POLL_INTERVAL_MS), which can be changed via the dashboard Config Editor or by editing config.env while the bot is running — no restart needed.
When Magellan starts, it creates a state.json file containing a units[] array with exactly SPLIT_NUMBER entries, each sized at PRINCIPAL / SPLIT_NUMBER USDC. The entire grid — unit count, unit sizing, buy targets — is derived from these structural values.
If you changed SPLIT_NUMBER from 10 to 15 while the bot was running, the state file would still have 10 units. The grid math would break: some units would be missing, buy targets would be wrong, and the buffer calculation would be off. The same applies to PRINCIPAL — existing units would have the old unit size baked in.
The only safe way to change structural parameters is: stop → edit → delete state → restart.
This is the simple case. All units are IDLE, no SOL is held, no transactions are in flight.
cd /var/www/magellan/current
cat state.json | python3 -c "
import json, sys
s = json.load(sys.stdin)
non_idle = [(i, u['status']) for i, u in enumerate(s.get('units', []), 1) if u['status'] != 'IDLE']
if non_idle:
print('NOT SAFE -- these units are not IDLE:')
for uid, status in non_idle:
print(f' Unit {uid}: {status}')
print('\nUse Scenario B instead.')
else:
print(f'All {len(s.get(\"units\", []))} units are IDLE -- safe to proceed')
"
If you want to track continuity, save these before deleting state:
cat state.json | python3 -c "
import json, sys
s = json.load(sys.stdin)
print('=== Lifetime Stats (save these for your records) ===')
print(f' Inception: {s.get(\"inceptionDate\", \"?\")}')
print(f' Lifetime P&L: {s.get(\"lifetimePnlUsdc\", 0):.4f} USDC')
print(f' Total trades: {s.get(\"lifetimeTotalTrades\", 0)}')
print(f' Winning trades: {s.get(\"lifetimeWinningTrades\", 0)}')
print(f' Losing trades: {s.get(\"lifetimeLosingTrades\", 0)}')
print(f' Cum. profit: {s.get(\"cumulativeProfitUsdc\", 0):.4f} USDC')
print(f' Transfers: {s.get(\"lifetimeTransfersUsdc\", 0):.4f} USDC')
"
Deleting
state.jsonresets all lifetime counters to zero. The dashboard will show a fresh inception date and zero P&L. Your historical performance is preserved in the JSONL log files underlog/— those are never deleted.
# Stop the bot
pm2 stop magellan
# Edit your parameters
nano config.env
# Change PRINCIPAL=xxx and/or SPLIT_NUMBER=yyy
# (change any other structural parameters as needed)
# Verify the new config is valid BEFORE deleting state
npm run build && node dist/tools/validateConfig.js
# If validateConfig reports errors, fix them before continuing
# Back up the old state (just in case) and delete it
cp state.json state.json.before-resize
rm state.json
# Restart -- bot creates fresh state with new parameters
pm2 restart magellan
# Verify the new grid
pm2 logs magellan --lines 20
Open the dashboard and confirm:
SPLIT_NUMBERPRINCIPAL / SPLIT_NUMBER valueThis is the case where the bot has bought SOL and is holding it, waiting to sell. You must not delete state.json until all positions are closed — otherwise the bot loses track of held SOL and those funds become stranded.
cd /var/www/magellan/current
# Enable kill switch -- stops new buys, allows existing positions to sell
touch HALT
You can also toggle the kill switch from the dashboard.
Monitor the units until everything is IDLE:
# Check current unit status
cat state.json | python3 -c "
import json, sys
s = json.load(sys.stdin)
for i, u in enumerate(s.get('units', []), 1):
status = u['status']
extra = ''
if status == 'HOLDING':
entry = u.get('entryPrice', 0)
sol = u.get('amountSol', 0)
extra = f' (entry: \${entry:.4f}, {sol:.6f} SOL)'
elif status == 'WAITING_TO_BUY':
target = u.get('targetBuyPrice', 0)
extra = f' (target: \${target:.4f})'
print(f' Unit {i}: {status}{extra}')
holding = sum(1 for u in s.get('units', []) if u['status'] == 'HOLDING')
waiting = sum(1 for u in s.get('units', []) if u['status'] == 'WAITING_TO_BUY')
print(f'\n Summary: {holding} HOLDING, {waiting} WAITING_TO_BUY')
if holding > 0:
print(' Wait for HOLDING units to hit take-profit or stop-loss')
else:
print(' No HOLDING units -- safe to proceed to Step 3')
"
How long will this take?
If you can't wait and need to force-close positions:
state.json — no real SOL is heldOnce all units are IDLE, follow the same steps as Scenario A:
# Verify all IDLE
cat state.json | python3 -c "
import json, sys
s = json.load(sys.stdin)
holding = sum(1 for u in s.get('units', []) if u['status'] != 'IDLE')
if holding > 0:
print(f'STOP -- {holding} units are not IDLE. Wait longer.')
sys.exit(1)
print('All units IDLE')
"
# Stop the bot
pm2 stop magellan
# Edit config
nano config.env
# Validate
npm run build && node dist/tools/validateConfig.js
# Back up and delete state
cp state.json state.json.before-resize
rm state.json
# Remove HALT file (clean start)
rm -f HALT
# Restart
pm2 restart magellan
# Before: PRINCIPAL=500, SPLIT_NUMBER=10 → $50/unit
# After: PRINCIPAL=1000, SPLIT_NUMBER=10 → $100/unit
Each unit now has twice the buying power. Grid spread stays the same. More USDC per trade means more absolute profit per fill, but also more absolute loss per stop-loss. Make sure your wallet has enough USDC for the new principal.
# Before: PRINCIPAL=500, SPLIT_NUMBER=10 → $50/unit
# After: PRINCIPAL=500, SPLIT_NUMBER=20 → $25/unit
More grid density — buys at more price levels, each smaller. This catches more oscillations but each fill profits less. Works well in choppy, range-bound markets. Check that the new unit size is above MIN_TRADE_USDC (default: $1) — validateConfig will catch this.
# Before: PRINCIPAL=1000, SPLIT_NUMBER=20 → $50/unit
# After: PRINCIPAL=500, SPLIT_NUMBER=10 → $50/unit
Keep the same unit size by reducing both proportionally. The grid becomes less dense but each position is the same size. Suitable when you want to reduce exposure.
# Before: PRINCIPAL=500, SPLIT_NUMBER=10, PRINCIPAL_LEFTOVER=10
# → 9 active units, 1 buffer, $50/unit
# After: PRINCIPAL=500, SPLIT_NUMBER=10, PRINCIPAL_LEFTOVER=20
# → 8 active units, 2 buffer, $50/unit
More buffer means fewer active units but more capital held in reserve. This is conservative — fewer positions fill, but you have a larger cushion against drawdowns.
PRINCIPAL=50
SPLIT_NUMBER=5
PRINCIPAL_LEFTOVER=10
# → 4-5 active units, $10/unit
Unit size must be above MIN_TRADE_USDC. With the default MIN_TRADE_USDC=1, the smallest practical setup is around $5/unit. Below that, Jupiter quote rounding and transaction fees eat into returns significantly.
Switching MODE is also a structural change, but it has additional safety checks built in.
The bot detects paper HOLDING units on startup in live mode and automatically resets them to IDLE with a warning. This prevents the bot from trying to sell SOL on-chain that was never actually purchased.
# You'll see this in the logs:
[WARN] Unit 3 is HOLDING with paper signature -- resetting to IDLE (paper positions are not real on-chain)
Recommended procedure: Still follow Scenario A or B above (close all positions first, delete state). The auto-reset is a safety net, not the intended workflow.
If you switch from live to paper mode while HOLDING real SOL, the bot will "sell" those positions in paper mode — meaning it simulates the sell but your real SOL stays in your wallet. The bot's P&L accounting will be wrong because it thinks it sold, but no on-chain transaction happened.
Always close all live positions before switching to paper mode.
Before starting:
During the change:
pm2 stop magellan)config.env has been edited with new valuesvalidateConfig passes with new parametersstate.json is backed up (state.json.before-resize)state.json is deletedAfter restart:
pm2 logs shows clean startup with new parametersLOOP_ERROR or unexpected warnings in logsQ: Can I change PRINCIPAL without changing SPLIT_NUMBER?
Yes. Each unit will be resized to new_PRINCIPAL / SPLIT_NUMBER. Follow the same procedure.
Q: Can I change just PRINCIPAL_LEFTOVER without touching PRINCIPAL or SPLIT_NUMBER?
Yes, but it still requires the full stop-delete-restart procedure because it changes how many units are active vs buffer.
Q: What if I delete state.json while units are HOLDING in live mode?
The bot forgets about those positions. Your SOL is still in your wallet — it's not lost on-chain. But Magellan won't manage it anymore. You'd need to manually swap the SOL back to USDC via Jupiter UI, Phantom, or another wallet tool.
Q: Do I lose my log history?
No. Log files in log/YYYY/MM/ are never affected by state deletion. Your entire trade history is preserved there. The dashboard Logs tab and History tab will still show all past trades.
Q: Can I edit state.json manually instead of deleting it?
Technically possible but strongly discouraged. The state file has internal consistency requirements (unit IDs, array length matching SPLIT_NUMBER, amountUsdc matching unit size, grid center vs current price). A manual edit that gets any of these wrong can cause silent bugs. Deleting and letting the bot create a fresh state is always safer.
Q: What about settings_history.jsonl?
Keep it. The bot appends a new CONFIG_SNAPSHOT on startup with the new parameters. The Settings tab will show the change in the configuration history timeline.