refactor: Migrate to improved fuzzing system with visualizer support
- Replaced old FuzzingAnalysis.s.sol with improved RecordedFuzzingAnalysis - Old fuzzing used trades too small (0.1-100% of remaining balance) to reach discovery - New system uses larger trades (50-200 ETH) that successfully find invariants - run-fuzzing.sh now redirects to run-recorded-fuzzing.sh for backward compatibility - Added position CSV generation for profitable scenarios to support visualizer - Visualizer automatically launches when invariants are found - Removed unnecessary debugCSV complexity The old fuzzing couldn't find invariants because trades got progressively smaller (often <1 ETH after a few iterations) and couldn't move price the 3,690 ticks needed to reach the discovery position. The new system maintains large trade sizes throughout the scenario, successfully finding profitable exploits. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2c69963151
commit
c1627dd4c9
4 changed files with 79 additions and 792 deletions
|
|
@ -1,384 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import {TestEnvironment} from "../test/helpers/TestBase.sol";
|
||||
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import {IWETH9} from "../src/interfaces/IWETH9.sol";
|
||||
import {Kraiken} from "../src/Kraiken.sol";
|
||||
import {Stake} from "../src/Stake.sol";
|
||||
import {LiquidityManager} from "../src/LiquidityManager.sol";
|
||||
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
|
||||
import "../test/mocks/BullMarketOptimizer.sol";
|
||||
import "../test/mocks/NeutralMarketOptimizer.sol";
|
||||
import "../test/mocks/BearMarketOptimizer.sol";
|
||||
import "../test/mocks/WhaleOptimizer.sol";
|
||||
import "../test/mocks/MockOptimizer.sol";
|
||||
import "../test/mocks/RandomScenarioOptimizer.sol";
|
||||
import "./helpers/CSVManager.sol";
|
||||
import "./helpers/SwapExecutor.sol";
|
||||
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
|
||||
/**
|
||||
* @title FuzzingAnalysis
|
||||
* @notice Fuzzing analysis to find profitable trading scenarios against LiquidityManager
|
||||
* @dev Configurable via environment variables:
|
||||
* - FUZZING_RUNS: Number of fuzzing iterations per market (default 100)
|
||||
* - TRACK_POSITIONS: Track detailed position data (default false)
|
||||
*/
|
||||
contract FuzzingAnalysis is Test, CSVManager {
|
||||
TestEnvironment testEnv;
|
||||
IUniswapV3Factory factory;
|
||||
IUniswapV3Pool pool;
|
||||
IWETH9 weth;
|
||||
Kraiken harberg;
|
||||
Stake stake;
|
||||
LiquidityManager lm;
|
||||
bool token0isWeth;
|
||||
|
||||
address account = makeAddr("trader");
|
||||
address feeDestination = makeAddr("fees");
|
||||
|
||||
// Analysis metrics
|
||||
uint256 public scenariosAnalyzed;
|
||||
uint256 public profitableScenarios;
|
||||
|
||||
// Configuration
|
||||
uint256 public fuzzingRuns;
|
||||
bool public trackPositions;
|
||||
string public optimizerClass;
|
||||
uint256 public tradesPerRun;
|
||||
uint256 public seedOffset;
|
||||
|
||||
// Optimizers
|
||||
BullMarketOptimizer bullOptimizer;
|
||||
NeutralMarketOptimizer neutralOptimizer;
|
||||
BearMarketOptimizer bearOptimizer;
|
||||
WhaleOptimizer whaleOptimizer;
|
||||
MockOptimizer mockOptimizer;
|
||||
RandomScenarioOptimizer randomOptimizer;
|
||||
|
||||
function run() public {
|
||||
_loadConfiguration();
|
||||
|
||||
console.log("=== Fuzzing Analysis ===");
|
||||
console.log(string.concat("Optimizer: ", optimizerClass));
|
||||
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
|
||||
console.log(string.concat("Trades per run: ", vm.toString(tradesPerRun)));
|
||||
console.log(string.concat("Position tracking: ", trackPositions ? "enabled" : "disabled"));
|
||||
console.log("");
|
||||
|
||||
testEnv = new TestEnvironment(feeDestination);
|
||||
|
||||
// Get optimizer based on class name
|
||||
address optimizerAddress = _getOptimizerByClass(optimizerClass);
|
||||
|
||||
// Initialize CSV for profitable scenarios
|
||||
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %\n";
|
||||
uint256 profitableCount;
|
||||
uint256 marketProfitable = 0;
|
||||
|
||||
console.log(string.concat("=== FUZZING with ", optimizerClass, " ==="));
|
||||
|
||||
for (uint256 seed = seedOffset; seed < seedOffset + fuzzingRuns; seed++) {
|
||||
if (seed % 10 == 0 && seed > 0) {
|
||||
console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns)));
|
||||
}
|
||||
|
||||
// Create fresh environment for each run
|
||||
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
|
||||
testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress);
|
||||
|
||||
// Fund LiquidityManager with initial ETH
|
||||
vm.deal(address(lm), 50 ether);
|
||||
|
||||
// Fund account with random amount (10-50 ETH)
|
||||
uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether);
|
||||
vm.deal(account, fundAmount * 2);
|
||||
vm.prank(account);
|
||||
weth.deposit{value: fundAmount}();
|
||||
|
||||
uint256 initialBalance = weth.balanceOf(account);
|
||||
|
||||
// Initial recenter
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {} catch {}
|
||||
|
||||
// Run trading scenario
|
||||
uint256 finalBalance = _runFuzzedScenario(optimizerClass, seed);
|
||||
|
||||
scenariosAnalyzed++;
|
||||
|
||||
// Calculate profit/loss
|
||||
bool isProfitable = finalBalance > initialBalance;
|
||||
uint256 profitOrLoss;
|
||||
uint256 profitOrLossPercentage;
|
||||
|
||||
if (isProfitable) {
|
||||
profitOrLoss = finalBalance - initialBalance;
|
||||
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
|
||||
profitableScenarios++;
|
||||
marketProfitable++;
|
||||
|
||||
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitOrLossPercentage), "%"));
|
||||
|
||||
// Add to CSV
|
||||
profitableCSV = string.concat(
|
||||
profitableCSV,
|
||||
optimizerClass, ",",
|
||||
vm.toString(seed), ",",
|
||||
vm.toString(initialBalance), ",",
|
||||
vm.toString(finalBalance), ",",
|
||||
vm.toString(profitOrLoss), ",",
|
||||
vm.toString(profitOrLossPercentage), "\n"
|
||||
);
|
||||
profitableCount++;
|
||||
} else {
|
||||
profitOrLoss = initialBalance - finalBalance;
|
||||
profitOrLossPercentage = (profitOrLoss * 100) / initialBalance;
|
||||
}
|
||||
|
||||
// Always log result for cumulative tracking
|
||||
console.log(string.concat("RESULT|SEED:", vm.toString(seed), "|INITIAL:", vm.toString(initialBalance), "|FINAL:", vm.toString(finalBalance), "|PNL:", isProfitable ? "+" : "-", vm.toString(profitOrLoss)));
|
||||
}
|
||||
|
||||
console.log(string.concat("\nResults for ", optimizerClass, ":"));
|
||||
console.log(string.concat("Profitable: ", vm.toString(marketProfitable), "/", vm.toString(fuzzingRuns)));
|
||||
console.log("");
|
||||
|
||||
console.log("=== ANALYSIS COMPLETE ===");
|
||||
console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed)));
|
||||
console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios)));
|
||||
console.log(string.concat("Profitable rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%"));
|
||||
|
||||
// Write profitable scenarios CSV if any found
|
||||
if (profitableCount > 0) {
|
||||
console.log("Writing profitable scenarios CSV...");
|
||||
string memory filename = string.concat("profitable_scenarios_", vm.toString(block.timestamp), ".csv");
|
||||
vm.writeFile(filename, profitableCSV);
|
||||
console.log(string.concat("\nProfitable scenarios written to: ", filename));
|
||||
} else {
|
||||
console.log("\nNo profitable scenarios found.");
|
||||
}
|
||||
|
||||
console.log("Script execution complete.");
|
||||
}
|
||||
|
||||
function _loadConfiguration() internal {
|
||||
fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(100));
|
||||
trackPositions = vm.envOr("TRACK_POSITIONS", false);
|
||||
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
|
||||
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20));
|
||||
seedOffset = vm.envOr("SEED_OFFSET", uint256(0));
|
||||
}
|
||||
|
||||
function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) {
|
||||
// Initialize position tracking CSV if enabled
|
||||
if (trackPositions) {
|
||||
initializePositionsCSV();
|
||||
_recordPositionData("Initial");
|
||||
}
|
||||
|
||||
// Use seed for randomness
|
||||
uint256 rand = uint256(keccak256(abi.encodePacked(seed, scenarioName, block.timestamp)));
|
||||
|
||||
// Use configured number of trades (with some randomness)
|
||||
uint256 numTrades = tradesPerRun + (rand % 11) - 5; // +/- 5 trades
|
||||
if (numTrades < 5) numTrades = 5; // Minimum 5 trades
|
||||
|
||||
// Initial buy if no HARB
|
||||
if (harberg.balanceOf(account) == 0 && weth.balanceOf(account) > 0) {
|
||||
uint256 initialBuy = weth.balanceOf(account) / 10;
|
||||
_executeBuy(initialBuy);
|
||||
}
|
||||
|
||||
// Execute random trades
|
||||
for (uint256 i = 0; i < numTrades; i++) {
|
||||
rand = uint256(keccak256(abi.encodePacked(rand, i)));
|
||||
uint256 action = rand % 100;
|
||||
|
||||
if (action < 25) { // 25% chance buy
|
||||
uint256 wethBal = weth.balanceOf(account);
|
||||
if (wethBal > 0) {
|
||||
uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100%
|
||||
uint256 buyAmount = (wethBal * buyPercent) / 1000;
|
||||
if (buyAmount > 0) {
|
||||
_executeBuy(buyAmount);
|
||||
if (trackPositions) {
|
||||
_recordPositionData(string.concat("Buy_", vm.toString(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (action < 50) { // 25% chance sell
|
||||
uint256 harbBal = harberg.balanceOf(account);
|
||||
if (harbBal > 0) {
|
||||
uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100%
|
||||
uint256 sellAmount = (harbBal * sellPercent) / 1000;
|
||||
if (sellAmount > 0) {
|
||||
_executeSell(sellAmount);
|
||||
if (trackPositions) {
|
||||
_recordPositionData(string.concat("Sell_", vm.toString(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // 50% chance recenter
|
||||
uint256 waitTime = 1 minutes + (rand % 10 hours);
|
||||
vm.warp(block.timestamp + waitTime);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
if (trackPositions) {
|
||||
_recordPositionData(string.concat("Recenter_", vm.toString(i)));
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Skip trades at extreme ticks
|
||||
(, int24 currentTick, , , , , ) = pool.slot0();
|
||||
if (currentTick < -887000 || currentTick > 887000) continue;
|
||||
}
|
||||
|
||||
// Sell remaining HARB
|
||||
uint256 finalHarb = harberg.balanceOf(account);
|
||||
if (finalHarb > 0) {
|
||||
_executeSell(finalHarb);
|
||||
if (trackPositions) {
|
||||
_recordPositionData("Final_Sell");
|
||||
}
|
||||
}
|
||||
|
||||
// Final recenters
|
||||
for (uint256 j = 0; j < 1 + (rand % 3); j++) {
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {} catch {}
|
||||
}
|
||||
|
||||
// Write position tracking CSV if enabled
|
||||
if (trackPositions) {
|
||||
_recordPositionData("Final");
|
||||
string memory positionFilename = string.concat(
|
||||
"positions_", scenarioName, "_", vm.toString(seed), ".csv"
|
||||
);
|
||||
writeCSVToFile(positionFilename);
|
||||
console.log(string.concat("Position tracking CSV written to: ", positionFilename));
|
||||
}
|
||||
|
||||
return weth.balanceOf(account);
|
||||
}
|
||||
|
||||
function _executeBuy(uint256 amount) internal {
|
||||
if (amount == 0 || weth.balanceOf(account) < amount) return;
|
||||
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
vm.prank(account);
|
||||
weth.transfer(address(executor), amount);
|
||||
|
||||
try executor.executeBuy(amount, account) {} catch {}
|
||||
}
|
||||
|
||||
function _executeSell(uint256 amount) internal {
|
||||
if (amount == 0 || harberg.balanceOf(account) < amount) return;
|
||||
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
vm.prank(account);
|
||||
harberg.transfer(address(executor), amount);
|
||||
|
||||
try executor.executeSell(amount, account) {} catch {}
|
||||
}
|
||||
|
||||
function _getOrCreateOptimizer(uint256 index) internal returns (address) {
|
||||
if (index == 0) {
|
||||
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
|
||||
return address(bullOptimizer);
|
||||
} else if (index == 1) {
|
||||
if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer();
|
||||
return address(neutralOptimizer);
|
||||
} else {
|
||||
if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer();
|
||||
return address(bearOptimizer);
|
||||
}
|
||||
}
|
||||
|
||||
function _getOptimizerByClass(string memory className) internal returns (address) {
|
||||
bytes32 classHash = keccak256(abi.encodePacked(className));
|
||||
|
||||
if (classHash == keccak256(abi.encodePacked("BullMarketOptimizer"))) {
|
||||
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
|
||||
return address(bullOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("NeutralMarketOptimizer"))) {
|
||||
if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer();
|
||||
return address(neutralOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("BearMarketOptimizer"))) {
|
||||
if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer();
|
||||
return address(bearOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("WhaleOptimizer"))) {
|
||||
if (address(whaleOptimizer) == address(0)) whaleOptimizer = new WhaleOptimizer();
|
||||
return address(whaleOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("MockOptimizer"))) {
|
||||
if (address(mockOptimizer) == address(0)) {
|
||||
mockOptimizer = new MockOptimizer();
|
||||
mockOptimizer.initialize(address(harberg), address(stake));
|
||||
}
|
||||
return address(mockOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("RandomScenarioOptimizer"))) {
|
||||
if (address(randomOptimizer) == address(0)) randomOptimizer = new RandomScenarioOptimizer();
|
||||
return address(randomOptimizer);
|
||||
} else {
|
||||
revert(string.concat("Unknown optimizer class: ", className, ". Available: BullMarketOptimizer, NeutralMarketOptimizer, BearMarketOptimizer, WhaleOptimizer, MockOptimizer, RandomScenarioOptimizer"));
|
||||
}
|
||||
}
|
||||
|
||||
function _recordPositionData(string memory label) internal {
|
||||
(,int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Cap currentTick to avoid overflow in extreme cases
|
||||
if (currentTick > 887000) currentTick = 887000;
|
||||
if (currentTick < -887000) currentTick = -887000;
|
||||
|
||||
// Get each position
|
||||
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
|
||||
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
||||
|
||||
// Debug: Log liquidity values
|
||||
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
|
||||
console.log("=== LIQUIDITY VALUES ===");
|
||||
console.log("Label:", label);
|
||||
console.log("Current tick:", uint256(int256(currentTick)));
|
||||
console.log("Anchor range:", uint256(int256(anchorLower)), "-", uint256(int256(anchorUpper)));
|
||||
console.log("Anchor liquidity:", uint256(anchorLiq));
|
||||
console.log("Discovery range:", uint256(int256(discoveryLower)), "-", uint256(int256(discoveryUpper)));
|
||||
console.log("Discovery liquidity:", uint256(discoveryLiq));
|
||||
if (uint256(anchorLiq) > 0) {
|
||||
console.log("Discovery/Anchor liquidity ratio:", uint256(discoveryLiq) * 100 / uint256(anchorLiq), "%");
|
||||
console.log("Anchor width:", uint256(int256(anchorUpper - anchorLower)), "ticks");
|
||||
console.log("Discovery width:", uint256(int256(discoveryUpper - discoveryLower)), "ticks");
|
||||
uint256 anchorLiqPerTick = uint256(anchorLiq) * 1000 / uint256(int256(anchorUpper - anchorLower));
|
||||
uint256 discoveryLiqPerTick = uint256(discoveryLiq) * 1000 / uint256(int256(discoveryUpper - discoveryLower));
|
||||
console.log("Anchor liquidity per tick (x1000):", anchorLiqPerTick);
|
||||
console.log("Discovery liquidity per tick (x1000):", discoveryLiqPerTick);
|
||||
console.log("Discovery/Anchor per tick ratio:", discoveryLiqPerTick * 100 / anchorLiqPerTick, "%");
|
||||
}
|
||||
}
|
||||
|
||||
// Create position data row with liquidity values directly
|
||||
string memory row = string.concat(
|
||||
label, ", ",
|
||||
vm.toString(currentTick), ", ",
|
||||
vm.toString(floorLower), ", ",
|
||||
vm.toString(floorUpper), ", ",
|
||||
vm.toString(uint256(floorLiq)), ", ",
|
||||
vm.toString(anchorLower), ", ",
|
||||
vm.toString(anchorUpper), ", ",
|
||||
vm.toString(uint256(anchorLiq)), ", ",
|
||||
vm.toString(discoveryLower), ", ",
|
||||
vm.toString(discoveryUpper), ", ",
|
||||
vm.toString(uint256(discoveryLiq)), ", ",
|
||||
token0isWeth ? "true" : "false"
|
||||
);
|
||||
appendCSVRow(row);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,8 @@ contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis {
|
|||
// Initialize recorder for this scenario
|
||||
if (enableRecording) {
|
||||
recorder = new ScenarioRecorder();
|
||||
// Always track positions when recording to generate CSV for visualizer
|
||||
trackPositions = true;
|
||||
recorder.initializeScenario(
|
||||
seed,
|
||||
optimizerClass,
|
||||
|
|
|
|||
|
|
@ -1,408 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Change to the analysis directory (where this script is located)
|
||||
cd "$(dirname "$0")"
|
||||
# This script now uses the improved recorded fuzzing system
|
||||
# The old FuzzingAnalysis.s.sol has been replaced with RecordedFuzzingAnalysis.s.sol
|
||||
# which uses larger trades that can actually reach the discovery position
|
||||
|
||||
# Function to cleanup on exit
|
||||
cleanup() {
|
||||
if [ -n "$VIEWER_PID" ]; then
|
||||
echo -e "\n${YELLOW}Stopping viewer...${NC}"
|
||||
# Kill the entire process group
|
||||
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
|
||||
sleep 1
|
||||
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
|
||||
fi
|
||||
rm -f profitable_scenario.csv 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Set trap to cleanup on script exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Default values
|
||||
OPTIMIZER_CLASS=""
|
||||
TOTAL_RUNS=50
|
||||
TRADES_PER_RUN=20
|
||||
DEBUG_CSV=false
|
||||
WHALE_MODE=false
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "Usage: $0 <optimizer_class> [runs=N] [trades=N] [debugCSV] [whaleMode]"
|
||||
echo ""
|
||||
echo "Parameters:"
|
||||
echo " optimizer_class Required. The optimizer class to use"
|
||||
echo " runs=N Optional. Number of fuzzing runs (default: 50)"
|
||||
echo " trades=N Optional. Trades per run (default: 20, actual will be ±5)"
|
||||
echo " debugCSV Optional. Enable debug mode with position tracking CSV (forces runs=1)"
|
||||
echo " whaleMode Optional. Enable whale-sized trades (20-80% of balance, 50-500 ETH funding)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 BullMarketOptimizer"
|
||||
echo " $0 WhaleOptimizer runs=100"
|
||||
echo " $0 BearMarketOptimizer runs=10 trades=50"
|
||||
echo " $0 NeutralMarketOptimizer trades=30 runs=25"
|
||||
echo " $0 BullMarketOptimizer debugCSV"
|
||||
echo " $0 WhaleOptimizer whaleMode runs=20"
|
||||
echo ""
|
||||
echo "Available optimizers:"
|
||||
echo " - BullMarketOptimizer"
|
||||
echo " - NeutralMarketOptimizer"
|
||||
echo " - BearMarketOptimizer"
|
||||
echo " - WhaleOptimizer"
|
||||
echo " - MockOptimizer"
|
||||
echo " - RandomScenarioOptimizer"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Error: No optimizer class specified"
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First argument is always the optimizer class
|
||||
OPTIMIZER_CLASS=$1
|
||||
shift
|
||||
|
||||
# Parse named parameters
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
runs=*)
|
||||
TOTAL_RUNS="${arg#*=}"
|
||||
if ! [[ "$TOTAL_RUNS" =~ ^[0-9]+$ ]] || [ "$TOTAL_RUNS" -eq 0 ]; then
|
||||
echo "Error: Invalid value for runs. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
trades=*)
|
||||
TRADES_PER_RUN="${arg#*=}"
|
||||
if ! [[ "$TRADES_PER_RUN" =~ ^[0-9]+$ ]] || [ "$TRADES_PER_RUN" -eq 0 ]; then
|
||||
echo "Error: Invalid value for trades. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
debugCSV)
|
||||
DEBUG_CSV=true
|
||||
TOTAL_RUNS=1
|
||||
;;
|
||||
whaleMode)
|
||||
WHALE_MODE=true
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown parameter '$arg'"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
OUTPUT_DIR="fuzzing_results_${OPTIMIZER_CLASS}_$(date +%Y%m%d_%H%M%S)"
|
||||
MERGED_CSV="$OUTPUT_DIR/merged_profitable_scenarios.csv"
|
||||
|
||||
echo -e "${GREEN}=== Fuzzing Campaign ===${NC}"
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
if [ "$DEBUG_CSV" = true ]; then
|
||||
echo -e "${YELLOW}Debug mode: ENABLED (position tracking CSV will be generated)${NC}"
|
||||
fi
|
||||
if [ "$WHALE_MODE" = true ]; then
|
||||
echo -e "${YELLOW}Whale mode: ENABLED (20-80% trades, 50-500 ETH funding)${NC}"
|
||||
fi
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo "Note: run-fuzzing.sh now uses the improved fuzzing with recording capabilities"
|
||||
echo "Redirecting to run-recorded-fuzzing.sh..."
|
||||
echo ""
|
||||
|
||||
# Validate that the optimizer class exists by doing a dry run
|
||||
echo "Validating optimizer class..."
|
||||
OPTIMIZER_CLASS="$OPTIMIZER_CLASS" FUZZING_RUNS=1 TRADES_PER_RUN=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > /tmp/optimizer_check.log 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
# Check specifically for unknown optimizer error
|
||||
if grep -q "Unknown optimizer class" /tmp/optimizer_check.log; then
|
||||
echo -e "${RED}Error: Invalid optimizer class '${OPTIMIZER_CLASS}'${NC}"
|
||||
echo -e "${RED}Check the error:${NC}"
|
||||
grep "Unknown optimizer" /tmp/optimizer_check.log
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
else
|
||||
# Other errors are ok during validation, we just want to check the optimizer exists
|
||||
echo "Optimizer validation passed (non-optimizer errors ignored)"
|
||||
fi
|
||||
else
|
||||
echo "Optimizer validation passed"
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Initialize merged CSV with header
|
||||
echo "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %" > "$MERGED_CSV"
|
||||
|
||||
# Track statistics
|
||||
TOTAL_PROFITABLE=0
|
||||
FAILED_RUNS=0
|
||||
CUMULATIVE_PNL=0
|
||||
CSV_GENERATED=false
|
||||
LATEST_CSV=""
|
||||
|
||||
# Save configuration
|
||||
CONFIG_FILE="$OUTPUT_DIR/config.txt"
|
||||
{
|
||||
echo "Fuzzing Configuration"
|
||||
echo "===================="
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
echo "Start time: $(date)"
|
||||
} > "$CONFIG_FILE"
|
||||
|
||||
# Run fuzzing analysis multiple times
|
||||
for i in $(seq 1 $TOTAL_RUNS); do
|
||||
echo -e "${YELLOW}Running fuzzing iteration $i/$TOTAL_RUNS...${NC}"
|
||||
|
||||
# Run single fuzzing iteration with specified optimizer and trades
|
||||
# Use iteration number as seed offset to ensure different scenarios
|
||||
# Enable position tracking if debugCSV is set
|
||||
if [ "$DEBUG_CSV" = true ]; then
|
||||
WHALE_MODE="$WHALE_MODE" TRACK_POSITIONS=true SEED_OFFSET=$((i - 1)) OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1
|
||||
else
|
||||
WHALE_MODE="$WHALE_MODE" SEED_OFFSET=$((i - 1)) OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Run $i completed${NC}"
|
||||
|
||||
# Extract P&L from RESULT line (may have leading spaces from forge output)
|
||||
RESULT_LINE=$(grep "RESULT|" "$OUTPUT_DIR/run_$i.log")
|
||||
if [ -n "$RESULT_LINE" ]; then
|
||||
# Parse the RESULT line to extract P&L
|
||||
PNL_VALUE=$(echo "$RESULT_LINE" | awk -F'|' '{print $5}' | sed 's/PNL://' | tr -d ' ')
|
||||
|
||||
# Debug: show values before calculation
|
||||
# echo "DEBUG: Current cumulative: $CUMULATIVE_PNL, New P&L: $PNL_VALUE"
|
||||
|
||||
# Add to cumulative P&L using awk (handles large numbers better than bash arithmetic)
|
||||
CUMULATIVE_PNL=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" -v pnl="$PNL_VALUE" 'BEGIN {printf "%.0f", cum + pnl}')
|
||||
|
||||
# Format cumulative P&L for display (convert from wei to ETH)
|
||||
CUMULATIVE_ETH=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" 'BEGIN {printf "%.6f", cum / 1000000000000000000}')
|
||||
|
||||
# Check if profitable
|
||||
if [[ "$PNL_VALUE" == +* ]]; then
|
||||
echo -e "${GREEN} Found profitable scenario!${NC}"
|
||||
((TOTAL_PROFITABLE++))
|
||||
|
||||
# Extract profit percentage
|
||||
PROFIT_PCT=$(grep "PROFITABLE!" "$OUTPUT_DIR/run_$i.log" | grep -oE "Profit: [0-9]+%" | grep -oE "[0-9]+")
|
||||
echo -e "${GREEN} Profit: ${PROFIT_PCT}%${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} Loss scenario${NC}"
|
||||
fi
|
||||
|
||||
# Display cumulative P&L
|
||||
if awk -v cum="$CUMULATIVE_PNL" 'BEGIN {exit !(cum >= 0)}'; then
|
||||
echo -e "${GREEN} Cumulative P&L: +${CUMULATIVE_ETH} ETH${NC}"
|
||||
else
|
||||
echo -e "${RED} Cumulative P&L: ${CUMULATIVE_ETH} ETH${NC}"
|
||||
fi
|
||||
|
||||
# Extract CSV file path if generated (for profitable scenarios)
|
||||
CSV_FILE=$(grep "Profitable scenarios written to:" "$OUTPUT_DIR/run_$i.log" | awk '{print $NF}')
|
||||
if [ -n "$CSV_FILE" ] && [ -f "$CSV_FILE" ]; then
|
||||
# Append data rows (skip header) to merged CSV
|
||||
tail -n +2 "$CSV_FILE" >> "$MERGED_CSV"
|
||||
# Move individual CSV to output directory
|
||||
mv "$CSV_FILE" "$OUTPUT_DIR/"
|
||||
CSV_GENERATED=true
|
||||
LATEST_CSV="$OUTPUT_DIR/$(basename "$CSV_FILE")"
|
||||
fi
|
||||
|
||||
# In debug mode, also look for position tracking CSV
|
||||
if [ "$DEBUG_CSV" = true ]; then
|
||||
# Look for position CSV mentioned in the log
|
||||
POSITION_CSV=$(grep "Position tracking CSV written to:" "$OUTPUT_DIR/run_$i.log" | awk -F': ' '{print $2}')
|
||||
if [ -n "$POSITION_CSV" ]; then
|
||||
# The CSV is generated in the parent directory (onchain), so check there
|
||||
PARENT_CSV="../$POSITION_CSV"
|
||||
if [ -f "$PARENT_CSV" ]; then
|
||||
echo -e "${GREEN} Position tracking CSV generated: $POSITION_CSV${NC}"
|
||||
# Move to output directory with a more descriptive name
|
||||
FINAL_CSV_NAME="debug_positions_${OPTIMIZER_CLASS}_seed${SEED_OFFSET}.csv"
|
||||
mv "$PARENT_CSV" "$OUTPUT_DIR/$FINAL_CSV_NAME"
|
||||
echo -e "${GREEN} Moved to: $OUTPUT_DIR/$FINAL_CSV_NAME${NC}"
|
||||
CSV_GENERATED=true
|
||||
LATEST_CSV="$OUTPUT_DIR/$FINAL_CSV_NAME"
|
||||
elif [ -f "$POSITION_CSV" ]; then
|
||||
# Fallback if it's in the current directory
|
||||
echo -e "${GREEN} Position tracking CSV generated: $POSITION_CSV${NC}"
|
||||
FINAL_CSV_NAME="debug_positions_${OPTIMIZER_CLASS}_seed${SEED_OFFSET}.csv"
|
||||
mv "$POSITION_CSV" "$OUTPUT_DIR/$FINAL_CSV_NAME"
|
||||
echo -e "${GREEN} Moved to: $OUTPUT_DIR/$FINAL_CSV_NAME${NC}"
|
||||
CSV_GENERATED=true
|
||||
LATEST_CSV="$OUTPUT_DIR/$FINAL_CSV_NAME"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW} Warning: No RESULT line found in output${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ Run $i failed${NC}"
|
||||
((FAILED_RUNS++))
|
||||
# Show last few lines of error
|
||||
echo -e "${RED}Error details:${NC}"
|
||||
tail -5 "$OUTPUT_DIR/run_$i.log"
|
||||
fi
|
||||
|
||||
# Small delay to avoid overwhelming the system
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
# Update config with end time
|
||||
echo "End time: $(date)" >> "$CONFIG_FILE"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}=== FUZZING CAMPAIGN COMPLETE ===${NC}"
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS))"
|
||||
echo "Failed runs: $FAILED_RUNS"
|
||||
echo "Total profitable scenarios: $TOTAL_PROFITABLE"
|
||||
|
||||
# Display final cumulative P&L
|
||||
FINAL_CUMULATIVE_ETH=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" 'BEGIN {printf "%.6f", cum / 1000000000000000000}')
|
||||
if awk -v cum="$CUMULATIVE_PNL" 'BEGIN {exit !(cum >= 0)}'; then
|
||||
echo -e "${GREEN}Final Cumulative P&L: +${FINAL_CUMULATIVE_ETH} ETH${NC}"
|
||||
else
|
||||
echo -e "${RED}Final Cumulative P&L: ${FINAL_CUMULATIVE_ETH} ETH${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results saved in: $OUTPUT_DIR"
|
||||
echo "Merged CSV: $MERGED_CSV"
|
||||
|
||||
# Generate summary report
|
||||
SUMMARY="$OUTPUT_DIR/summary.txt"
|
||||
{
|
||||
echo "Fuzzing Campaign Summary"
|
||||
echo "========================"
|
||||
echo "Date: $(date)"
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
echo ""
|
||||
echo "Results:"
|
||||
echo "--------"
|
||||
echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS)) / $TOTAL_RUNS"
|
||||
echo "Failed runs: $FAILED_RUNS"
|
||||
echo "Total profitable scenarios: $TOTAL_PROFITABLE / $((TOTAL_RUNS - FAILED_RUNS))"
|
||||
echo "Success rate: $(awk "BEGIN {if ($TOTAL_RUNS - $FAILED_RUNS > 0) printf \"%.2f\", $TOTAL_PROFITABLE/($TOTAL_RUNS-$FAILED_RUNS)*100; else print \"0.00\"}")%"
|
||||
echo ""
|
||||
echo "Profit/Loss Analysis:"
|
||||
echo "--------------------"
|
||||
echo "Cumulative P&L: $FINAL_CUMULATIVE_ETH ETH"
|
||||
echo "Average P&L per run: $(awk -v cumeth="$FINAL_CUMULATIVE_ETH" -v total="$TOTAL_RUNS" -v failed="$FAILED_RUNS" 'BEGIN {if (total - failed > 0) printf "%.6f ETH", cumeth/(total-failed); else print "0.000000 ETH"}')"
|
||||
} > "$SUMMARY"
|
||||
|
||||
echo ""
|
||||
echo "Summary report: $SUMMARY"
|
||||
|
||||
# If there were profitable scenarios, show a sample
|
||||
if [ $TOTAL_PROFITABLE -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}Sample profitable scenarios:${NC}"
|
||||
head -5 "$MERGED_CSV"
|
||||
fi
|
||||
|
||||
# If debug mode was used, mention the position tracking CSV
|
||||
if [ "$DEBUG_CSV" = true ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}Debug position tracking CSV generated!${NC}"
|
||||
echo "View it with: ./view-scenarios.sh"
|
||||
echo "Then navigate to the output directory and select the debug CSV file"
|
||||
fi
|
||||
|
||||
# If any CSV was generated, launch the viewer
|
||||
if [ "$CSV_GENERATED" = true ] && [ -n "$LATEST_CSV" ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}=== Launching Scenario Visualizer ===${NC}"
|
||||
echo "CSV file: $LATEST_CSV"
|
||||
|
||||
# Create a temporary symlink to the CSV for the viewer
|
||||
TEMP_LINK="profitable_scenario.csv"
|
||||
if [ -f "$TEMP_LINK" ] || [ -L "$TEMP_LINK" ]; then
|
||||
rm -f "$TEMP_LINK"
|
||||
fi
|
||||
# Use absolute path for the symlink
|
||||
ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK"
|
||||
|
||||
# Check if server is already running on common ports
|
||||
SERVER_RUNNING=false
|
||||
EXISTING_PORT=""
|
||||
for PORT in 8000 8001 8002; do
|
||||
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||||
# Check if it's a python http server in our analysis directory
|
||||
if lsof -Pi :$PORT -sTCP:LISTEN 2>/dev/null | grep -q "python.*http.server"; then
|
||||
SERVER_RUNNING=true
|
||||
EXISTING_PORT=$PORT
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$SERVER_RUNNING" = true ]; then
|
||||
echo -e "${YELLOW}Server already running on port $EXISTING_PORT${NC}"
|
||||
echo -e "${GREEN}Browser should open to: http://localhost:$EXISTING_PORT/scenario-visualizer.html${NC}"
|
||||
|
||||
# Try to open browser to existing server
|
||||
if command -v xdg-open &> /dev/null; then
|
||||
xdg-open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
|
||||
elif command -v open &> /dev/null; then
|
||||
open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}"
|
||||
read -r
|
||||
else
|
||||
# Start the viewer in background in its own process group
|
||||
setsid ./view-scenarios.sh &
|
||||
VIEWER_PID=$!
|
||||
|
||||
# Give the server time to start and browser to open
|
||||
sleep 2
|
||||
|
||||
# Show the URL
|
||||
echo ""
|
||||
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
|
||||
echo "(If port 8000 was busy, check the port number mentioned above)"
|
||||
echo "If browser didn't open, manually navigate to that URL"
|
||||
|
||||
# Wait for user input
|
||||
echo ""
|
||||
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
|
||||
read -r
|
||||
|
||||
# Kill the viewer process and its children
|
||||
if [ -n "$VIEWER_PID" ]; then
|
||||
# Kill the entire process group (includes python server)
|
||||
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
|
||||
# Give it a moment to clean up
|
||||
sleep 1
|
||||
# Force kill if still running
|
||||
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Viewer stopped.${NC}"
|
||||
fi
|
||||
|
||||
# Clean up the symlink
|
||||
rm -f "$TEMP_LINK"
|
||||
fi
|
||||
# Pass all arguments to the new script
|
||||
exec ./analysis/run-recorded-fuzzing.sh "$@"
|
||||
|
|
@ -58,7 +58,6 @@ mkdir -p $OUTPUT_DIR
|
|||
echo -e "${YELLOW}Starting recorded fuzzing analysis...${NC}"
|
||||
FUZZING_RUNS=$RUNS_VALUE \
|
||||
OPTIMIZER_CLASS=$OPTIMIZER \
|
||||
TRACK_POSITIONS=true \
|
||||
RUN_ID=$RUN_ID \
|
||||
forge script analysis/RecordedFuzzingAnalysis.s.sol:RecordedFuzzingAnalysis -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log
|
||||
|
||||
|
|
@ -106,9 +105,9 @@ if [ $SCENARIO_COUNT -gt 0 ]; then
|
|||
done
|
||||
|
||||
# Move position CSVs if they exist
|
||||
if ls positions_*.csv 1> /dev/null 2>&1; then
|
||||
mv positions_*.csv $OUTPUT_DIR/
|
||||
echo -e "\n Position CSVs moved to: ${BLUE}$OUTPUT_DIR/${NC}"
|
||||
if ls improved_positions_*.csv 1> /dev/null 2>&1; then
|
||||
mv improved_positions_*.csv $OUTPUT_DIR/
|
||||
echo -e "\n Position CSVs for visualization moved to: ${BLUE}$OUTPUT_DIR/${NC}"
|
||||
fi
|
||||
|
||||
# Create index file
|
||||
|
|
@ -157,4 +156,70 @@ fi
|
|||
|
||||
echo ""
|
||||
echo -e "${GREEN}Results saved to: $OUTPUT_DIR/${NC}"
|
||||
echo -e "Run ID: ${BOLD}${RUN_ID}${NC}"
|
||||
echo -e "Run ID: ${BOLD}${RUN_ID}${NC}"
|
||||
|
||||
# Launch visualizer if position CSVs were generated
|
||||
POSITION_CSV_COUNT=$(ls -1 $OUTPUT_DIR/improved_positions_*.csv 2>/dev/null | wc -l)
|
||||
|
||||
if [ $POSITION_CSV_COUNT -gt 0 ] && [ $SCENARIO_COUNT -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}=== Launching Scenario Visualizer ===${NC}"
|
||||
|
||||
# Get the first position CSV for visualization
|
||||
FIRST_CSV=$(ls -1 $OUTPUT_DIR/improved_positions_*.csv 2>/dev/null | head -1)
|
||||
echo "CSV file: $FIRST_CSV"
|
||||
|
||||
# Create a temporary symlink to the CSV for the viewer
|
||||
TEMP_LINK="profitable_scenario.csv"
|
||||
if [ -f "$TEMP_LINK" ] || [ -L "$TEMP_LINK" ]; then
|
||||
rm -f "$TEMP_LINK"
|
||||
fi
|
||||
# Use absolute path for the symlink
|
||||
ln -s "$(pwd)/$FIRST_CSV" "$TEMP_LINK"
|
||||
|
||||
# Check if server is already running on common ports
|
||||
SERVER_RUNNING=false
|
||||
EXISTING_PORT=""
|
||||
for PORT in 8000 8001 8002; do
|
||||
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||||
# Check if it's a python http server in our analysis directory
|
||||
if lsof -Pi :$PORT -sTCP:LISTEN 2>/dev/null | grep -q "python.*http.server"; then
|
||||
SERVER_RUNNING=true
|
||||
EXISTING_PORT=$PORT
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$SERVER_RUNNING" = true ]; then
|
||||
echo -e "${YELLOW}Server already running on port $EXISTING_PORT${NC}"
|
||||
echo -e "${GREEN}Open browser to: http://localhost:$EXISTING_PORT/scenario-visualizer.html${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}"
|
||||
read -r
|
||||
else
|
||||
# Start the viewer in background
|
||||
./analysis/view-scenarios.sh &
|
||||
VIEWER_PID=$!
|
||||
|
||||
# Give the server time to start
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
|
||||
read -r
|
||||
|
||||
# Kill the viewer process
|
||||
if [ -n "$VIEWER_PID" ]; then
|
||||
pkill -P $VIEWER_PID 2>/dev/null || true
|
||||
kill $VIEWER_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Viewer stopped.${NC}"
|
||||
fi
|
||||
|
||||
# Clean up the symlink
|
||||
rm -f "$TEMP_LINK"
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue