#
tokens: 14263/50000 13/13 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── jest.config.js
├── package.json
├── README.md
├── src
│   ├── config
│   │   └── env.ts
│   ├── index.ts
│   ├── services
│   │   ├── api.ts
│   │   ├── mongodb.ts
│   │   └── tools.ts
│   ├── tools
│   │   └── analyze-volume-walls.ts
│   └── types
│       └── tools.ts
├── tsconfig.json
├── vld-logo.png
└── volume_wall_detector.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | package-lock.json
 7 | yarn.lock
 8 | 
 9 | # Build outputs
10 | dist/
11 | build/
12 | *.tsbuildinfo
13 | 
14 | # Environment variables
15 | .env
16 | .env.local
17 | .env.*.local
18 | 
19 | # IDE and editor files
20 | .idea/
21 | .vscode/
22 | *.swp
23 | *.swo
24 | .DS_Store
25 | 
26 | # Test coverage
27 | coverage/
28 | .nyc_output/
29 | 
30 | # Logs
31 | logs/
32 | *.log
33 | 
34 | # Temporary files
35 | tmp/
36 | temp/
37 | 
38 | # Azure specific
39 | *.pem
40 | *.pfx
41 | *.cer
42 | *.crt
43 | *.key
44 | 
45 | # TypeScript
46 | *.tsbuildinfo
47 | 
48 | # Optional npm cache directory
49 | .npm
50 | 
51 | # Optional eslint cache
52 | .eslintcache
53 | 
54 | # Optional REPL history
55 | .node_repl_history
56 | 
57 | # Output of 'npm pack'
58 | *.tgz
59 | 
60 | # Yarn Integrity file
61 | .yarn-integrity 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Volume Wall Detector MCP Server 📊
  2 | 
  3 | > 🔌 **Compatible with Cline, Cursor, Claude Desktop, and any other MCP Clients!**
  4 | > 
  5 | > Volume Wall Detector MCP works seamlessly with any MCP client
  6 | 
  7 | <p align="center">
  8 |   <img src="vld-logo.png" width="300" alt="VLD Logo">
  9 | </p>
 10 | 
 11 | The Model Context Protocol (MCP) is an open standard that enables AI systems to interact seamlessly with various data sources and tools, facilitating secure, two-way connections.
 12 | 
 13 | The Volume Wall Detector MCP server provides:
 14 | 
 15 | * Real-time stock trading volume analysis
 16 | * Detection of significant price levels (volume walls)
 17 | * Trading imbalance tracking and analysis
 18 | * After-hours trading analysis
 19 | * MongoDB-based data persistence
 20 | 
 21 | ## Prerequisites 🔧
 22 | 
 23 | Before you begin, ensure you have:
 24 | 
 25 | * MongoDB instance running
 26 | * Stock market API access
 27 | * Node.js (v20 or higher)
 28 | * Git installed (only needed if using Git installation method)
 29 | 
 30 | ## Volume Wall Detector MCP Server Installation ⚡
 31 | 
 32 | ### Running with NPX
 33 | 
 34 | ```bash
 35 | npx -y volume-wall-detector-mcp@latest
 36 | ```
 37 | 
 38 | ### Installing via Smithery
 39 | 
 40 | To install Volume Wall Detector MCP Server for Claude Desktop automatically via Smithery:
 41 | 
 42 | ```bash
 43 | npx -y @smithery/cli install volume-wall-detector-mcp --client claude
 44 | ```
 45 | 
 46 | ## Configuring MCP Clients ⚙️
 47 | 
 48 | ### Configuring Cline 🤖
 49 | 
 50 | 1. Open the Cline MCP settings file:
 51 | ```bash
 52 | # For macOS:
 53 | code ~/Library/Application\ Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
 54 | 
 55 | # For Windows:
 56 | code %APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json
 57 | ```
 58 | 
 59 | 2. Add the Volume Wall Detector server configuration:
 60 | ```json
 61 | {
 62 |   "mcpServers": {
 63 |     "volume-wall-detector-mcp": {
 64 |       "command": "npx",
 65 |       "args": ["-y", "volume-wall-detector-mcp@latest"],
 66 |       "env": {
 67 |         "TIMEZONE": "GMT+7",
 68 |         "API_BASE_URL": "your-api-url-here",
 69 |         "MONGO_HOST": "localhost",
 70 |         "MONGO_PORT": "27017",
 71 |         "MONGO_DATABASE": "volume_wall_detector",
 72 |         "MONGO_USER": "admin",
 73 |         "MONGO_PASSWORD": "password",
 74 |         "MONGO_AUTH_SOURCE": "admin",
 75 |         "MONGO_AUTH_MECHANISM": "SCRAM-SHA-1",
 76 |         "PAGE_SIZE": "50",
 77 |         "TRADES_TO_FETCH": "10000",
 78 |         "DAYS_TO_FETCH": "1",
 79 |         "TRANSPORT_TYPE": "stdio",
 80 |         "PORT": "8080"
 81 |       },
 82 |       "disabled": false,
 83 |       "autoApprove": []
 84 |     }
 85 |   }
 86 | }
 87 | ```
 88 | 
 89 | ### Configuring Cursor 🖥️
 90 | 
 91 | > **Note**: Requires Cursor version 0.45.6 or higher
 92 | 
 93 | 1. Open Cursor Settings
 94 | 2. Navigate to Open MCP
 95 | 3. Click on "Add New Global MCP Server"
 96 | 4. Fill out the following information:
 97 |    * **Name**: "volume-wall-detector-mcp"
 98 |    * **Type**: "command"
 99 |    * **Command**:
100 |    ```bash
101 |    env TIMEZONE=GMT+7 API_BASE_URL=your-api-url-here MONGO_HOST=localhost MONGO_PORT=27017 MONGO_DATABASE=volume_wall_detector MONGO_USER=admin MONGO_PASSWORD=password MONGO_AUTH_SOURCE=admin MONGO_AUTH_MECHANISM=SCRAM-SHA-1 PAGE_SIZE=50 TRADES_TO_FETCH=10000 DAYS_TO_FETCH=1 npx -y volume-wall-detector-mcp@latest
102 |    ```
103 | 
104 | ### Configuring Claude Desktop 🖥️
105 | 
106 | Create or edit the Claude Desktop configuration file:
107 | 
108 | #### For macOS:
109 | ```bash
110 | code "$HOME/Library/Application Support/Claude/claude_desktop_config.json"
111 | ```
112 | 
113 | #### For Windows:
114 | ```bash
115 | code %APPDATA%\Claude\claude_desktop_config.json
116 | ```
117 | 
118 | Add the configuration:
119 | ```json
120 | {
121 |   "mcpServers": {
122 |     "volume-wall-detector-mcp": {
123 |       "command": "npx",
124 |       "args": ["-y", "volume-wall-detector-mcp@latest"],
125 |       "env": {
126 |         "TIMEZONE": "GMT+7",
127 |         "API_BASE_URL": "your-api-url-here",
128 |         "MONGO_HOST": "localhost",
129 |         "MONGO_PORT": "27017",
130 |         "MONGO_DATABASE": "volume_wall_detector",
131 |         "MONGO_USER": "admin",
132 |         "MONGO_PASSWORD": "password",
133 |         "MONGO_AUTH_SOURCE": "admin",
134 |         "MONGO_AUTH_MECHANISM": "SCRAM-SHA-1",
135 |         "PAGE_SIZE": "50",
136 |         "TRADES_TO_FETCH": "10000",
137 |         "DAYS_TO_FETCH": "1",
138 |         "TRANSPORT_TYPE": "stdio",
139 |         "PORT": "8080"
140 |       }
141 |     }
142 |   }
143 | }
144 | ```
145 | 
146 | ## License
147 | 
148 | MIT 
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | module.exports = {
 2 |   preset: "ts-jest",
 3 |   testEnvironment: "node",
 4 |   moduleFileExtensions: ["ts", "js"],
 5 |   transform: {
 6 |     "^.+\\.ts$": "ts-jest",
 7 |   },
 8 |   testMatch: ["**/__tests__/**/*.test.ts"],
 9 |   moduleNameMapper: {
10 |     "^@/(.*)$": "<rootDir>/src/$1",
11 |   },
12 | }; 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "CommonJS",
 5 |     "lib": ["ES2020"],
 6 |     "declaration": true,
 7 |     "outDir": "./dist",
 8 |     "rootDir": "./src",
 9 |     "strict": true,
10 |     "esModuleInterop": true,
11 |     "skipLibCheck": true,
12 |     "forceConsistentCasingInFileNames": true,
13 |     "moduleResolution": "node",
14 |     "resolveJsonModule": true,
15 |     "isolatedModules": true,
16 |     "noEmit": false
17 |   },
18 |   "include": ["src/**/*"],
19 |   "exclude": ["node_modules", "dist", "**/*.test.ts"]
20 | } 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | #!/usr/bin/env node
 2 | "use strict";
 3 | 
 4 | import { FastMCP } from "fastmcp";
 5 | import { tools } from "./services/tools";
 6 | import { Tool } from "./types/tools";
 7 | import { getConfig } from "./config/env";
 8 | 
 9 | const config = getConfig();
10 | 
11 | const server = new FastMCP({
12 |   name: "Volume Wall Detector MCP",
13 |   version: "1.0.0"
14 | });
15 | 
16 | // Register all tools
17 | tools.forEach((tool) => {
18 |   (server.addTool as Tool)(tool);
19 | });
20 | 
21 | // Start server with appropriate transport
22 | if (config.TRANSPORT_TYPE === "sse") {
23 |   server.start({
24 |     transportType: "sse",
25 |     sse: {
26 |       endpoint: "/sse",
27 |       port: parseInt(config.PORT, 10)
28 |     }
29 |   });
30 | } else {
31 |   server.start({
32 |     transportType: "stdio"
33 |   });
34 | } 
```

--------------------------------------------------------------------------------
/src/config/env.ts:
--------------------------------------------------------------------------------

```typescript
 1 | "use strict";
 2 | 
 3 | import { z } from "zod";
 4 | 
 5 | const envSchema = z.object({
 6 |   TIMEZONE: z.string().default("GMT+7"),
 7 |   API_BASE_URL: z.string().url(),
 8 |   MONGO_HOST: z.string(),
 9 |   MONGO_PORT: z.string(),
10 |   MONGO_DATABASE: z.string(),
11 |   MONGO_USER: z.string().optional(),
12 |   MONGO_PASSWORD: z.string().optional(),
13 |   MONGO_AUTH_SOURCE: z.string().optional(),
14 |   MONGO_AUTH_MECHANISM: z.string().optional(),
15 |   PAGE_SIZE: z.string().transform(Number).default("50"),
16 |   TRADES_TO_FETCH: z.string().transform(Number).default("10000"),
17 |   DAYS_TO_FETCH: z.string().transform(Number).default("1"),
18 |   TRANSPORT_TYPE: z.enum(["stdio", "sse"]).default("stdio"),
19 |   PORT: z.string().default("8080")
20 | });
21 | 
22 | export const getConfig = () => {
23 |   const result = envSchema.safeParse(process.env);
24 |   
25 |   if (!result.success) {
26 |     throw new Error(`Configuration error: ${result.error.message}`);
27 |   }
28 |   
29 |   return result.data;
30 | }; 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "volume-wall-detector-mcp",
 3 |   "version": "1.0.1",
 4 |   "description": "Volume Wall Detector MCP Server",
 5 |   "main": "dist/index.js",
 6 |   "type": "commonjs",
 7 |   "bin": {
 8 |     "volume-wall-detector-mcp": "dist/index.js"
 9 |   },
10 |   "files": [
11 |     "dist",
12 |     "README.md",
13 |     "LICENSE"
14 |   ],
15 |   "scripts": {
16 |     "dev": "tsx src/index.ts",
17 |     "build": "tsc",
18 |     "start": "node dist/index.js",
19 |     "test": "jest",
20 |     "test:watch": "jest --watch",
21 |     "test:coverage": "jest --coverage",
22 |     "prepare": "npm run build"
23 |   },
24 |   "keywords": ["trading", "volume", "analysis", "mcp"],
25 |   "author": "",
26 |   "license": "ISC",
27 |   "dependencies": {
28 |     "axios": "^1.6.7",
29 |     "dotenv": "^16.4.5",
30 |     "fastmcp": "^1.0.0",
31 |     "mongodb": "^6.3.0",
32 |     "zod": "^3.22.4"
33 |   },
34 |   "devDependencies": {
35 |     "@types/jest": "^29.5.12",
36 |     "@types/node": "^20.11.19",
37 |     "jest": "^29.7.0",
38 |     "ts-jest": "^29.1.2",
39 |     "tsx": "^4.7.1",
40 |     "typescript": "^5.3.3"
41 |   }
42 | } 
```

--------------------------------------------------------------------------------
/src/services/tools.ts:
--------------------------------------------------------------------------------

```typescript
 1 | "use strict";
 2 | 
 3 | import { z } from "zod";
 4 | import { ToolConfig } from "../types/tools";
 5 | import { analyzeVolumeWalls } from "../tools/analyze-volume-walls";
 6 | import { fetchOrderBook, fetchTrades } from "./api";
 7 | import { storeStockData } from "./mongodb";
 8 | 
 9 | export const tools: ToolConfig[] = [
10 |   {
11 |     name: "fetch-order-book",
12 |     description: "Fetch current order book data for a symbol",
13 |     parameters: z.object({
14 |       symbol: z.string().describe("Stock symbol to fetch order book for")
15 |     }),
16 |     execute: async (args) => {
17 |       const orderBook = await fetchOrderBook(args.symbol);
18 |       const result = await storeStockData(orderBook, "order_books");
19 |       return JSON.stringify(result);
20 |     }
21 |   },
22 |   {
23 |     name: "fetch-trades",
24 |     description: "Fetch recent trades for a symbol",
25 |     parameters: z.object({
26 |       symbol: z.string().describe("Stock symbol to fetch trades for")
27 |     }),
28 |     execute: async (args) => {
29 |       const trades = await fetchTrades(args.symbol);
30 |       const result = await storeStockData(trades, "trades");
31 |       return JSON.stringify(result);
32 |     }
33 |   },
34 |   {
35 |     name: "analyze-stock",
36 |     description: "Analyze stock data including volume and value analysis",
37 |     parameters: z.object({
38 |       symbol: z.string().describe("Stock symbol to analyze"),
39 |       days: z.number().optional().describe("Number of days to analyze (optional)")
40 |     }),
41 |     execute: async (args) => {
42 |       const result = await analyzeVolumeWalls(args.symbol, args.days);
43 |       return JSON.stringify(result);
44 |     }
45 |   }
46 | ]; 
```

--------------------------------------------------------------------------------
/src/types/tools.ts:
--------------------------------------------------------------------------------

```typescript
 1 | "use strict";
 2 | 
 3 | import { z } from "zod";
 4 | import { FastMCP } from "fastmcp";
 5 | 
 6 | export type ToolConfig = {
 7 |   name: string;
 8 |   description: string;
 9 |   parameters: z.ZodObject<any>;
10 |   execute: (args: any) => Promise<string>;
11 | };
12 | 
13 | export type Tool = FastMCP["addTool"];
14 | 
15 | // Data Models
16 | export const OrderBookLevelSchema = z.object({
17 |   price: z.number(),
18 |   volume: z.number()
19 | });
20 | 
21 | export const OrderBookSchema = z.object({
22 |   symbol: z.string(),
23 |   timestamp: z.string(),
24 |   match_price: z.number(),
25 |   bid_1: OrderBookLevelSchema,
26 |   ask_1: OrderBookLevelSchema,
27 |   change_percent: z.number(),
28 |   volume: z.number()
29 | });
30 | 
31 | export const TradeSchema = z.object({
32 |   trade_id: z.string(),
33 |   symbol: z.string(),
34 |   price: z.number(),
35 |   volume: z.number(),
36 |   side: z.enum(["bu", "sd", "after-hour"]),
37 |   time: z.number()
38 | });
39 | 
40 | export const PriceVolumeDataSchema = z.object({
41 |   buy_volume: z.number().default(0),
42 |   sell_volume: z.number().default(0),
43 |   after_hour_buy: z.number().default(0),
44 |   after_hour_sell: z.number().default(0),
45 |   after_hour_unknown: z.number().default(0),
46 |   buy_value: z.number().default(0),
47 |   sell_value: z.number().default(0),
48 |   after_hour_buy_value: z.number().default(0),
49 |   after_hour_sell_value: z.number().default(0),
50 |   after_hour_unknown_value: z.number().default(0),
51 |   total_volume: z.number().default(0),
52 |   total_value: z.number().default(0),
53 |   volume_imbalance: z.number().default(0),
54 |   value_imbalance: z.number().default(0),
55 |   total_trades: z.number().default(0),
56 |   last_trade_time: z.string().optional()
57 | });
58 | 
59 | export type OrderBookLevel = z.infer<typeof OrderBookLevelSchema>;
60 | export type OrderBook = z.infer<typeof OrderBookSchema>;
61 | export type Trade = z.infer<typeof TradeSchema>;
62 | export type PriceVolumeData = z.infer<typeof PriceVolumeDataSchema>; 
```

--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------

```typescript
 1 | "use strict";
 2 | 
 3 | import axios from "axios";
 4 | import { getConfig } from "../config/env";
 5 | import { OrderBook, Trade } from "../types/tools";
 6 | 
 7 | const config = getConfig();
 8 | 
 9 | const headers = {
10 |   "User-Agent": "Mozilla/5.0",
11 |   "Accept": "application/json"
12 | };
13 | 
14 | export const fetchOrderBook = async (symbol: string): Promise<OrderBook> => {
15 |   const url = `${config.API_BASE_URL}/v2/stock/${symbol}`;
16 |   const response = await axios.get(url, { headers });
17 |   
18 |   const data = response.data.data;
19 |   return {
20 |     symbol,
21 |     timestamp: new Date().toISOString(),
22 |     match_price: data.mp,
23 |     bid_1: {
24 |       price: data.b1,
25 |       volume: data.b1v
26 |     },
27 |     ask_1: {
28 |       price: data.o1,
29 |       volume: data.o1v
30 |     },
31 |     change_percent: data.lpcp,
32 |     volume: data.lv
33 |   };
34 | };
35 | 
36 | export const fetchTrades = async (symbol: string): Promise<Trade[]> => {
37 |   const trades: Trade[] = [];
38 |   let lastId: string | undefined;
39 |   
40 |   while (trades.length < config.TRADES_TO_FETCH) {
41 |     const params: Record<string, any> = {
42 |       stockSymbol: symbol,
43 |       pageSize: Math.min(config.PAGE_SIZE, config.TRADES_TO_FETCH - trades.length)
44 |     };
45 |     
46 |     if (lastId) {
47 |       params.lastId = lastId;
48 |     }
49 |     
50 |     const url = `${config.API_BASE_URL}/le-table`;
51 |     const response = await axios.get(url, { headers, params });
52 |     
53 |     const items = response.data.data.items;
54 |     if (!items || items.length === 0) {
55 |       break;
56 |     }
57 |     
58 |     const batchTrades = items.map((item: any) => ({
59 |       trade_id: item._id,
60 |       symbol: item.stockSymbol,
61 |       price: item.price,
62 |       volume: item.vol,
63 |       side: item.side === "bu" || item.side === "sd" ? item.side : "after-hour",
64 |       time: Math.floor(new Date().setHours(
65 |         Number(item.time.split(":")[0]),
66 |         Number(item.time.split(":")[1]),
67 |         Number(item.time.split(":")[2] || 0)
68 |       ) / 1000)
69 |     }));
70 |     
71 |     trades.push(...batchTrades);
72 |     lastId = items[items.length - 1]._id;
73 |     
74 |     // Add small delay to avoid hitting rate limits
75 |     await new Promise(resolve => setTimeout(resolve, 100));
76 |   }
77 |   
78 |   return trades.slice(0, config.TRADES_TO_FETCH);
79 | }; 
```

--------------------------------------------------------------------------------
/src/services/mongodb.ts:
--------------------------------------------------------------------------------

```typescript
  1 | "use strict";
  2 | 
  3 | import { MongoClient } from "mongodb";
  4 | import { getConfig } from "../config/env";
  5 | import { OrderBook, Trade } from "../types/tools";
  6 | 
  7 | const config = getConfig();
  8 | 
  9 | export const getMongoUrl = () => {
 10 |   if (config.MONGO_USER && config.MONGO_PASSWORD) {
 11 |     return `mongodb://${config.MONGO_USER}:${config.MONGO_PASSWORD}@${config.MONGO_HOST}:${config.MONGO_PORT}/${config.MONGO_DATABASE}?authSource=${config.MONGO_AUTH_SOURCE}&authMechanism=${config.MONGO_AUTH_MECHANISM}`;
 12 |   }
 13 |   return `mongodb://${config.MONGO_HOST}:${config.MONGO_PORT}/${config.MONGO_DATABASE}`;
 14 | };
 15 | 
 16 | export const storeStockData = async (data: OrderBook | Trade[], collectionName: string) => {
 17 |   const client = new MongoClient(getMongoUrl());
 18 |   
 19 |   try {
 20 |     await client.connect();
 21 |     const db = client.db(config.MONGO_DATABASE);
 22 |     const collection = db.collection(collectionName);
 23 |     
 24 |     // Setup indexes if they don't exist
 25 |     if (collectionName === "order_books") {
 26 |       await collection.createIndex({ symbol: 1, timestamp: -1 });
 27 |     } else if (collectionName === "trades") {
 28 |       await collection.createIndex({ symbol: 1, time: -1 });
 29 |       await collection.createIndex({ trade_id: 1 }, { unique: true });
 30 |     }
 31 |     
 32 |     if (Array.isArray(data)) {
 33 |       if (data.length === 0) {
 34 |         return { success: true, inserted_count: 0 };
 35 |       }
 36 |       
 37 |       const operations = data.map(doc => ({
 38 |         updateOne: {
 39 |           filter: { trade_id: doc.trade_id },
 40 |           update: { $set: doc },
 41 |           upsert: true
 42 |         }
 43 |       }));
 44 |       
 45 |       const result = await collection.bulkWrite(operations);
 46 |       return {
 47 |         success: true,
 48 |         inserted_count: result.upsertedCount + result.modifiedCount
 49 |       };
 50 |     } else {
 51 |       const result = await collection.insertOne(data);
 52 |       return {
 53 |         success: result.acknowledged,
 54 |         inserted_count: result.acknowledged ? 1 : 0
 55 |       };
 56 |     }
 57 |   } catch (error) {
 58 |     return {
 59 |       success: false,
 60 |       error: error instanceof Error ? error.message : "Unknown error"
 61 |     };
 62 |   } finally {
 63 |     await client.close();
 64 |   }
 65 | };
 66 | 
 67 | export const getLatestOrderBook = async (symbol: string): Promise<OrderBook | null> => {
 68 |   const client = new MongoClient(getMongoUrl());
 69 |   
 70 |   try {
 71 |     await client.connect();
 72 |     const db = client.db(config.MONGO_DATABASE);
 73 |     const doc = await db.collection("order_books")
 74 |       .findOne({ symbol }, { sort: { timestamp: -1 } });
 75 |       
 76 |     return doc as OrderBook | null;
 77 |   } finally {
 78 |     await client.close();
 79 |   }
 80 | };
 81 | 
 82 | export const getRecentTrades = async (
 83 |   symbol: string,
 84 |   limit: number = 100,
 85 |   days: number = config.DAYS_TO_FETCH
 86 | ): Promise<Trade[]> => {
 87 |   const client = new MongoClient(getMongoUrl());
 88 |   
 89 |   try {
 90 |     await client.connect();
 91 |     const db = client.db(config.MONGO_DATABASE);
 92 |     
 93 |     const startTime = new Date();
 94 |     startTime.setDate(startTime.getDate() - days);
 95 |     startTime.setHours(0, 0, 0, 0);
 96 |     
 97 |     const trades = await db.collection("trades")
 98 |       .find({
 99 |         symbol,
100 |         time: { $gte: Math.floor(startTime.getTime() / 1000) }
101 |       })
102 |       .sort({ time: -1 })
103 |       .limit(limit)
104 |       .toArray();
105 |       
106 |     return trades.map(trade => ({
107 |       symbol: trade.symbol,
108 |       price: trade.price,
109 |       volume: trade.volume,
110 |       trade_id: trade.trade_id,
111 |       side: trade.side,
112 |       time: trade.time
113 |     }));
114 |   } finally {
115 |     await client.close();
116 |   }
117 | };
```

--------------------------------------------------------------------------------
/src/tools/analyze-volume-walls.ts:
--------------------------------------------------------------------------------

```typescript
  1 | "use strict";
  2 | 
  3 | import { OrderBook, Trade, PriceVolumeData } from "../types/tools";
  4 | import { getConfig } from "../config/env";
  5 | import { getLatestOrderBook, getRecentTrades } from "../services/mongodb";
  6 | 
  7 | const config = getConfig();
  8 | 
  9 | export const analyzeVolumeAtPrice = (
 10 |   trades: Trade[],
 11 |   orderBook: OrderBook
 12 | ): Record<number, PriceVolumeData> => {
 13 |   const priceVolumes: Record<number, PriceVolumeData> = {};
 14 |   
 15 |   for (const trade of trades.reverse()) {
 16 |     const price = trade.price;
 17 |     if (!priceVolumes[price]) {
 18 |       priceVolumes[price] = {
 19 |         buy_volume: 0,
 20 |         sell_volume: 0,
 21 |         after_hour_buy: 0,
 22 |         after_hour_sell: 0,
 23 |         after_hour_unknown: 0,
 24 |         buy_value: 0,
 25 |         sell_value: 0,
 26 |         after_hour_buy_value: 0,
 27 |         after_hour_sell_value: 0,
 28 |         after_hour_unknown_value: 0,
 29 |         total_volume: 0,
 30 |         total_value: 0,
 31 |         volume_imbalance: 0,
 32 |         value_imbalance: 0,
 33 |         total_trades: 0
 34 |       };
 35 |     }
 36 |     
 37 |     const data = priceVolumes[price];
 38 |     const value = trade.price * trade.volume;
 39 |     
 40 |     if (trade.side === "bu") {
 41 |       data.buy_volume += trade.volume;
 42 |       data.buy_value += value;
 43 |     } else if (trade.side === "sd") {
 44 |       data.sell_volume += trade.volume;
 45 |       data.sell_value += value;
 46 |     } else {
 47 |       if (trade.price >= orderBook.ask_1.price) {
 48 |         data.after_hour_buy += trade.volume;
 49 |         data.after_hour_buy_value += value;
 50 |       } else if (trade.price <= orderBook.bid_1.price) {
 51 |         data.after_hour_sell += trade.volume;
 52 |         data.after_hour_sell_value += value;
 53 |       } else {
 54 |         data.after_hour_unknown += trade.volume;
 55 |         data.after_hour_unknown_value += value;
 56 |       }
 57 |     }
 58 |     
 59 |     data.total_trades += 1;
 60 |     data.last_trade_time = new Date(trade.time * 1000).toISOString();
 61 |   }
 62 |   
 63 |   // Calculate totals and imbalances
 64 |   for (const data of Object.values(priceVolumes)) {
 65 |     data.total_volume = 
 66 |       data.buy_volume + 
 67 |       data.sell_volume + 
 68 |       data.after_hour_buy +
 69 |       data.after_hour_sell +
 70 |       data.after_hour_unknown;
 71 |       
 72 |     data.total_value = 
 73 |       data.buy_value + 
 74 |       data.sell_value + 
 75 |       data.after_hour_buy_value +
 76 |       data.after_hour_sell_value +
 77 |       data.after_hour_unknown_value;
 78 |       
 79 |     data.volume_imbalance = 
 80 |       (data.buy_volume + data.after_hour_buy) - 
 81 |       (data.sell_volume + data.after_hour_sell);
 82 |       
 83 |     data.value_imbalance = 
 84 |       (data.buy_value + data.after_hour_buy_value) - 
 85 |       (data.sell_value + data.after_hour_sell_value);
 86 |   }
 87 |   
 88 |   return priceVolumes;
 89 | };
 90 | 
 91 | export const analyzeVolumeWalls = async (symbol: string, days?: number) => {
 92 |   const orderBook = await getLatestOrderBook(symbol);
 93 |   if (!orderBook) {
 94 |     throw new Error("No order book data available");
 95 |   }
 96 |   
 97 |   const trades = await getRecentTrades(symbol, config.TRADES_TO_FETCH, days);
 98 |   const priceVolumes = analyzeVolumeAtPrice(trades, orderBook);
 99 |   
100 |   // Sort by total value
101 |   const sortedLevels = Object.entries(priceVolumes)
102 |     .sort(([, a], [, b]) => b.total_value - a.total_value)
103 |     .slice(0, 5);
104 |     
105 |   // Calculate trading summaries
106 |   const buyVolume = trades
107 |     .filter(t => t.side === "bu")
108 |     .reduce((sum, t) => sum + t.volume, 0);
109 |     
110 |   const sellVolume = trades
111 |     .filter(t => t.side === "sd")
112 |     .reduce((sum, t) => sum + t.volume, 0);
113 |     
114 |   const afterHourTrades = trades.filter(t => t.side === "after-hour");
115 |   const afterHourBuy = afterHourTrades
116 |     .filter(t => t.price >= orderBook.ask_1.price)
117 |     .reduce((sum, t) => sum + t.volume, 0);
118 |     
119 |   const afterHourSell = afterHourTrades
120 |     .filter(t => t.price <= orderBook.bid_1.price)
121 |     .reduce((sum, t) => sum + t.volume, 0);
122 |     
123 |   const afterHourUnknown = afterHourTrades
124 |     .filter(t => orderBook.bid_1.price < t.price && t.price < orderBook.ask_1.price)
125 |     .reduce((sum, t) => sum + t.volume, 0);
126 |     
127 |   const totalVolume = buyVolume + sellVolume + afterHourBuy + afterHourSell + afterHourUnknown;
128 |   
129 |   return {
130 |     timestamp: orderBook.timestamp,
131 |     symbol,
132 |     market_status: {
133 |       current_price: orderBook.match_price,
134 |       bid_price: orderBook.bid_1.price,
135 |       bid_volume: orderBook.bid_1.volume,
136 |       ask_price: orderBook.ask_1.price,
137 |       ask_volume: orderBook.ask_1.volume,
138 |       spread: orderBook.ask_1.price - orderBook.bid_1.price
139 |     },
140 |     volume_analysis: {
141 |       significant_levels: sortedLevels.map(([price, data]) => ({
142 |         price: Number(price),
143 |         ...data
144 |       })),
145 |       current_bid_accumulated: priceVolumes[orderBook.bid_1.price] || {},
146 |       current_ask_accumulated: priceVolumes[orderBook.ask_1.price] || {}
147 |     },
148 |     trading_summary: {
149 |       period: `last ${config.TRADES_TO_FETCH} trades`,
150 |       total_trades: trades.length,
151 |       volume: {
152 |         buy: buyVolume,
153 |         sell: sellVolume,
154 |         after_hour: {
155 |           buy: afterHourBuy,
156 |           sell: afterHourSell,
157 |           unknown: afterHourUnknown,
158 |           total: afterHourBuy + afterHourSell + afterHourUnknown
159 |         },
160 |         total: totalVolume,
161 |         buy_ratio: (buyVolume + afterHourBuy) / 
162 |           (buyVolume + sellVolume + afterHourBuy + afterHourSell) || 0
163 |       }
164 |     }
165 |   };
166 | }; 
```

--------------------------------------------------------------------------------
/volume_wall_detector.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | from datetime import date, datetime, timedelta, timezone
  3 | from dataclasses import dataclass, asdict
  4 | from typing import List, Optional, Union, Dict, Any
  5 | import pymongo
  6 | import requests
  7 | from pymongo import MongoClient
  8 | from dotenv import load_dotenv
  9 | import time
 10 | from pydantic import BaseModel, Field
 11 | 
 12 | load_dotenv()
 13 | 
 14 | # Mandatory environment variables
 15 | TIMEZONE = os.getenv("TIMEZONE", "GMT+7")  # Default to GMT+7 if not specified
 16 | API_BASE_URL = os.getenv("API_BASE_URL")
 17 | MONGO_HOST = os.getenv("MONGO_HOST")
 18 | MONGO_PORT = os.getenv("MONGO_PORT")
 19 | MONGO_DATABASE = os.getenv("MONGO_DATABASE")
 20 | MONGO_USER = os.getenv("MONGO_USER")
 21 | MONGO_PASSWORD = os.getenv("MONGO_PASSWORD")
 22 | MONGO_AUTH_SOURCE = os.getenv("MONGO_AUTH_SOURCE")
 23 | MONGO_AUTH_MECHANISM = os.getenv("MONGO_AUTH_MECHANISM")
 24 | 
 25 | # Optional environment variables
 26 | PAGE_SIZE = os.getenv("PAGE_SIZE", 50)
 27 | TRADES_TO_FETCH = int(os.getenv("TRADES_TO_FETCH", "10000"))
 28 | DAYS_TO_FETCH = int(os.getenv("DAYS_TO_FETCH", "1"))  # Default to 1 day if not specified
 29 | 
 30 | # Headers for API requests
 31 | HEADERS: dict = {
 32 |         "User-Agent": "Mozilla/5.0",
 33 |         "Accept": "application/json"
 34 |     }
 35 | 
 36 | def MONGO_URL() -> str:
 37 |         """Build MongoDB connection URL from components"""
 38 |         if MONGO_USER and MONGO_PASSWORD:
 39 |             return (
 40 |                 f"mongodb://{MONGO_USER}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}/{MONGO_DATABASE}"
 41 |                 f"?authSource={MONGO_AUTH_SOURCE}"
 42 |                 f"&authMechanism={MONGO_AUTH_MECHANISM}"
 43 |             )
 44 |         return f"mongodb://{MONGO_HOST}:{MONGO_PORT}/{MONGO_DATABASE}"
 45 | 
 46 | def parse_timezone(tz_str: str) -> timezone:
 47 |     """Parse timezone string (e.g., 'GMT+7' or 'GMT-5') into timezone object"""
 48 |     try:
 49 |         if not tz_str.startswith(('GMT+', 'GMT-')):
 50 |             raise ValueError("Timezone must be in format 'GMT+n' or 'GMT-n'")
 51 |         
 52 |         offset = int(tz_str[4:]) if tz_str[3] == '+' else -int(tz_str[4:])
 53 |         return timezone(timedelta(hours=offset))
 54 |     except Exception as e:
 55 |         raise ValueError(f"Invalid timezone format: {tz_str}. Error: {str(e)}")
 56 | 
 57 | class OrderBookLevel(BaseModel):
 58 |     """Order book level with price and volume"""
 59 |     price: float
 60 |     volume: int
 61 | 
 62 | class OrderBook(BaseModel):
 63 |     """Order book data"""
 64 |     symbol: str
 65 |     timestamp: str
 66 |     match_price: float
 67 |     bid_1: OrderBookLevel
 68 |     ask_1: OrderBookLevel
 69 |     change_percent: float
 70 |     volume: int
 71 | 
 72 | class Trade(BaseModel):
 73 |     """Trade data"""
 74 |     trade_id: str
 75 |     symbol: str
 76 |     price: float
 77 |     volume: int
 78 |     side: str  # "bu" or "sd" or "after-hour"
 79 |     time: int
 80 |     
 81 |     @property
 82 |     def value(self) -> float:
 83 |         """Calculate trade value"""
 84 |         return self.price * self.volume
 85 | 
 86 | class PriceVolumeData(BaseModel):
 87 |     """Volume and value data at a price level"""
 88 |     buy_volume: int = 0
 89 |     sell_volume: int = 0
 90 |     after_hour_buy: int = 0
 91 |     after_hour_sell: int = 0
 92 |     after_hour_unknown: int = 0
 93 |     buy_value: float = 0.0
 94 |     sell_value: float = 0.0
 95 |     after_hour_buy_value: float = 0.0
 96 |     after_hour_sell_value: float = 0.0
 97 |     after_hour_unknown_value: float = 0.0
 98 |     total_volume: int = 0
 99 |     total_value: float = 0.0
100 |     volume_imbalance: int = 0
101 |     value_imbalance: float = 0.0
102 |     total_trades: int = 0
103 |     last_trade_time: Optional[str] = None
104 | 
105 | class AfterHourVolume(BaseModel):
106 |     """After-hour trading volume data"""
107 |     buy: int
108 |     sell: int
109 |     unknown: int
110 |     total: int
111 | 
112 | class AfterHourValue(BaseModel):
113 |     """After-hour trading value data"""
114 |     buy: float
115 |     sell: float
116 |     unknown: float
117 |     total: float
118 | 
119 | class VolumeAnalysis(BaseModel):
120 |     """Volume analysis data"""
121 |     buy: int
122 |     sell: int
123 |     after_hour: AfterHourVolume
124 |     total: int
125 |     buy_ratio: float
126 | 
127 | class ValueAnalysis(BaseModel):
128 |     """Value analysis data"""
129 |     buy: float
130 |     sell: float
131 |     after_hour: AfterHourValue
132 |     total: float
133 |     buy_ratio: float
134 | 
135 | class MarketStatus(BaseModel):
136 |     """Current market status"""
137 |     current_price: float
138 |     bid_price: float
139 |     bid_volume: int
140 |     ask_price: float
141 |     ask_volume: int
142 |     spread: float
143 | 
144 | class VolumeAnalysisResult(BaseModel):
145 |     """Volume analysis results"""
146 |     significant_levels: list[Dict[str, Any]]
147 |     current_bid_accumulated: PriceVolumeData
148 |     current_ask_accumulated: PriceVolumeData
149 | 
150 | class TradingSummary(BaseModel):
151 |     """Trading summary data"""
152 |     period: str
153 |     total_trades: int
154 |     volume: VolumeAnalysis
155 |     value: ValueAnalysis
156 |     unique_price_levels: int
157 |     average_price: float
158 | 
159 | # class StockAnalysis(BaseModel):
160 | #     """Complete stock analysis result"""
161 | #     timestamp: str
162 | #     symbol: str
163 | #     market_status: MarketStatus
164 | #     volume_analysis: VolumeAnalysisResult
165 | #     trading_summary: TradingSummary
166 | 
167 | class MongoResult(BaseModel):
168 |     """MongoDB operation result"""
169 |     success: bool = False
170 |     inserted_count: int = 0
171 |     error: Optional[str] = None
172 | 
173 | class TradesResult(MongoResult):
174 |     """Result of trades operation"""
175 |     trades_fetched: int = 0
176 | 
177 | class StoreResult(BaseModel):
178 |     """Result of storing stock data"""
179 |     order_book: MongoResult
180 |     trades: TradesResult
181 | 
182 | def store_stock_data(data: Union[OrderBook, List[Trade]], collection_name: str) -> MongoResult:
183 |     """Store stock data into MongoDB"""
184 |     result = MongoResult()
185 |     
186 |     try:
187 |         client = MongoClient(MONGO_URL())
188 |         db = client[MONGO_DATABASE]
189 |         collection = db[collection_name]
190 |         
191 |         # Setup indexes if they don't exist
192 |         if collection_name == "order_books":
193 |             collection.create_index([("symbol", 1), ("timestamp", -1)])
194 |         elif collection_name == "trades":
195 |             collection.create_index([("symbol", 1), ("time", -1)])
196 |             collection.create_index([("trade_id", 1)], unique=True)
197 |         
198 |         # Convert and store data
199 |         if isinstance(data, OrderBook):
200 |             insert_result = collection.insert_one(data.model_dump())
201 |             result.success = insert_result.acknowledged
202 |             result.inserted_count = 1 if insert_result.acknowledged else 0
203 |             
204 |         elif isinstance(data, list):
205 |             if not data:
206 |                 result.success = True
207 |                 return result
208 |                 
209 |             trade_docs = [trade.model_dump() for trade in data]
210 |             try:
211 |                 operations = [
212 |                     pymongo.UpdateOne(
213 |                         {"trade_id": doc["trade_id"]},
214 |                         {"$set": doc},
215 |                         upsert=True
216 |                     ) for doc in trade_docs
217 |                 ]
218 |                 
219 |                 # Delete today's records using configured timezone
220 |                 tz = parse_timezone(TIMEZONE)
221 |                 today_start = datetime.now(tz).replace(
222 |                     hour=0, 
223 |                     minute=0, 
224 |                     second=0, 
225 |                     microsecond=0
226 |                 ).timestamp()
227 |                 
228 |                 collection.delete_many({
229 |                     "symbol": trade_docs[0]["symbol"],
230 |                     "time": {"$gte": today_start}
231 |                 })
232 |                 
233 |                 bulk_result = collection.bulk_write(operations, ordered=False)
234 |                 result.success = True
235 |                 result.inserted_count = bulk_result.upserted_count + bulk_result.modified_count
236 |             except Exception as e:
237 |                 result.success = False
238 |                 result.error = f"Bulk upsert failed: {str(e)}"
239 |                 
240 |     except Exception as e:
241 |         result.error = str(e)
242 |     finally:
243 |         client.close()
244 |         
245 |     return result
246 | 
247 | def fetch_order_book(symbol) -> OrderBook:
248 |     """Fetch current order book data for a symbol"""
249 |     url = f"{API_BASE_URL}/v2/stock/{symbol}"
250 |     response = requests.get(url, headers=HEADERS)
251 |     response.raise_for_status()
252 |     data = response.json().get("data", {})
253 |     
254 |     return OrderBook(
255 |         symbol=symbol,
256 |         timestamp=datetime.now().isoformat(),
257 |         match_price=data.get("mp"),
258 |         bid_1=OrderBookLevel(
259 |             price=data.get("b1"),
260 |             volume=data.get("b1v")
261 |         ),
262 |         ask_1=OrderBookLevel(
263 |             price=data.get("o1"),
264 |             volume=data.get("o1v")
265 |         ),
266 |         change_percent=data.get("lpcp"),
267 |         volume=data.get("lv")
268 |     )
269 | 
270 | def fetch_trades(symbol: str) -> List[Trade]:
271 |     """
272 |     Fetch specified number of trades for a symbol using lastId pagination
273 |     
274 |     Args:
275 |         symbol: Stock symbol
276 |         
277 |     Returns:
278 |         List[Trade]: List of trades, newest first
279 |     """
280 |     trades = []
281 |     last_id = None
282 |     
283 |     while len(trades) < TRADES_TO_FETCH:
284 |         # Prepare request parameters
285 |         params = {
286 |             "stockSymbol": symbol,
287 |             "pageSize": min(PAGE_SIZE, TRADES_TO_FETCH - len(trades))
288 |         }
289 |         if last_id:
290 |             params["lastId"] = last_id
291 |             
292 |         # Make API request
293 |         url = f"{API_BASE_URL}/le-table"
294 |         response = requests.get(url, headers=HEADERS, params=params)
295 |         response.raise_for_status()
296 |         
297 |         # Process response
298 |         items = response.json().get("data", {}).get("items", [])
299 |         if not items:  # No more trades available
300 |             break
301 |             
302 |         # Convert items to Trade objects
303 |         batch_trades = [
304 |             Trade(
305 |                 trade_id=item["_id"],
306 |                 symbol=item["stockSymbol"],
307 |                 price=item["price"],
308 |                 volume=item["vol"],
309 |                 side=item["side"] if item.get("side") in ["bu", "sd"] else "after-hour",
310 |                 time=datetime.combine(date.today(), datetime.strptime(item["time"], "%H:%M:%S").time()).timestamp()
311 |             ) for item in items
312 |         ]
313 |         
314 |         trades.extend(batch_trades)
315 |         
316 |         # Update last_id for next iteration
317 |         last_id = items[-1]["_id"]
318 |         
319 |         # Add small delay to avoid hitting rate limits
320 |         time.sleep(0.1)
321 |     
322 |     return trades[:TRADES_TO_FETCH]  # Ensure we don't return more than requested
323 | 
324 | def fetch_and_store_stock_data(symbol: str) -> StoreResult:
325 |     """
326 |     Fetch and store both order book and trades data
327 |     
328 |     Returns:
329 |         StoreResult: Results of both operations
330 |     """
331 |     # Fetch and store order book
332 |     order_book = fetch_order_book(symbol)
333 |     order_book_result = store_stock_data(order_book, "order_books")
334 |     
335 |     # Fetch and store trades
336 |     trades = fetch_trades(symbol)
337 |     trades_result = store_stock_data(trades, "trades")
338 |     
339 |     return StoreResult(
340 |         order_book=order_book_result,
341 |         trades=TradesResult(
342 |             success=trades_result.success,
343 |             inserted_count=trades_result.inserted_count,
344 |             error=trades_result.error,
345 |             trades_fetched=len(trades)
346 |         )
347 |     )
348 | 
349 | 
350 | def get_latest_order_book(symbol: str) -> Optional[OrderBook]:
351 |     """Get the latest order book from MongoDB"""
352 |     try:
353 |         client = MongoClient(MONGO_URL())
354 |         db = client[MONGO_DATABASE]
355 |         doc = db.order_books.find_one(
356 |             {"symbol": symbol},
357 |             sort=[("timestamp", -1)]
358 |         )
359 |         if doc:
360 |             return OrderBook(
361 |                 symbol=doc["symbol"],
362 |                 timestamp=doc["timestamp"],
363 |                 match_price=doc["match_price"],
364 |                 bid_1=OrderBookLevel(**doc["bid_1"]),
365 |                 ask_1=OrderBookLevel(**doc["ask_1"]),
366 |                 change_percent=doc["change_percent"],
367 |                 volume=doc["volume"]
368 |             )
369 |         return None
370 |     finally:
371 |         client.close()
372 | 
373 | def get_recent_trades(symbol: str, limit: int = 100, days: int = None) -> List[Trade]:
374 |     """
375 |     Get recent trades from MongoDB
376 |     
377 |     Args:
378 |         symbol: Stock symbol
379 |         limit: Maximum number of trades to return
380 |         days: Number of days to look back (defaults to DAYS_TO_FETCH from env)
381 |     
382 |     Returns:
383 |         List[Trade]: List of trades, newest first
384 |     """
385 |     try:
386 |         client = MongoClient(MONGO_URL())
387 |         db = client[MONGO_DATABASE]
388 |         
389 |         # Use provided days or fall back to environment variable
390 |         days_to_fetch = days if days is not None else DAYS_TO_FETCH
391 |         
392 |         # Calculate the timestamp for N days ago using configured timezone
393 |         tz = parse_timezone(TIMEZONE)
394 |         now = datetime.now(tz)
395 |         days_ago = now - timedelta(days=days_to_fetch)
396 |         start_timestamp = days_ago.replace(
397 |             hour=0, 
398 |             minute=0, 
399 |             second=0, 
400 |             microsecond=0
401 |         ).timestamp()
402 |         
403 |         # Query with date filter
404 |         trades = list(db.trades.find(
405 |             {
406 |                 "symbol": symbol,
407 |                 "time": {"$gte": start_timestamp}
408 |             },
409 |             sort=[("time", -1)],
410 |             limit=limit
411 |         ))
412 |         
413 |         return [Trade(
414 |             trade_id=t["trade_id"],
415 |             symbol=t["symbol"],
416 |             price=t["price"],
417 |             volume=t["volume"],
418 |             side=t["side"],
419 |             time=t["time"]
420 |         ) for t in trades]
421 |     finally:
422 |         client.close()
423 | 
424 | def analyze_volume_at_price(trades: List[Trade], order_book: OrderBook) -> Dict[float, PriceVolumeData]:
425 |     """Analyze accumulated volume and value at each price level"""
426 |     price_volumes: Dict[float, PriceVolumeData] = {}
427 |     
428 |     for trade in reversed(trades):
429 |         price = trade.price
430 |         if price not in price_volumes:
431 |             price_volumes[price] = PriceVolumeData()
432 |         
433 |         data = price_volumes[price]
434 |         # Accumulate volumes and values by side
435 |         if trade.side == "bu":
436 |             data.buy_volume += trade.volume
437 |             data.buy_value += trade.value
438 |         elif trade.side == "sd":
439 |             data.sell_volume += trade.volume
440 |             data.sell_value += trade.value
441 |         else:  # after-hour trade classification
442 |             if price >= order_book.ask_1.price:
443 |                 data.after_hour_buy += trade.volume
444 |                 data.after_hour_buy_value += trade.value
445 |             elif price <= order_book.bid_1.price:
446 |                 data.after_hour_sell += trade.volume
447 |                 data.after_hour_sell_value += trade.value
448 |             else:
449 |                 data.after_hour_unknown += trade.volume
450 |                 data.after_hour_unknown_value += trade.value
451 | 
452 |         tz = parse_timezone(TIMEZONE)
453 |         data.total_trades += 1
454 |         data.last_trade_time = str(datetime.fromtimestamp(trade.time, tz=tz))
455 |     
456 |     # Calculate additional metrics
457 |     for price_data in price_volumes.values():
458 |         price_data.total_volume = (
459 |             price_data.buy_volume + 
460 |             price_data.sell_volume + 
461 |             price_data.after_hour_buy +
462 |             price_data.after_hour_sell +
463 |             price_data.after_hour_unknown
464 |         )
465 |         price_data.total_value = (
466 |             price_data.buy_value + 
467 |             price_data.sell_value + 
468 |             price_data.after_hour_buy_value +
469 |             price_data.after_hour_sell_value +
470 |             price_data.after_hour_unknown_value
471 |         )
472 |         # Volume imbalance includes classified after-hour trades
473 |         price_data.volume_imbalance = (
474 |             price_data.buy_volume + 
475 |             price_data.after_hour_buy
476 |         ) - (
477 |             price_data.sell_volume + 
478 |             price_data.after_hour_sell
479 |         )
480 |         # Value imbalance includes classified after-hour trades
481 |         price_data.value_imbalance = (
482 |             price_data.buy_value + 
483 |             price_data.after_hour_buy_value
484 |         ) - (
485 |             price_data.sell_value + 
486 |             price_data.after_hour_sell_value
487 |         )
488 |     
489 |     return price_volumes
490 | 
491 | def analyze_stock_data(symbol: str, days: int = None) -> dict:
492 |     """Analyze stock data including volume and value analysis"""
493 |     order_book = get_latest_order_book(symbol)
494 |     trades = get_recent_trades(symbol, limit=TRADES_TO_FETCH, days=days)
495 |     
496 |     if not order_book:
497 |         raise ValueError("No order book data available")
498 |     
499 |     # Analyze volumes at each price level
500 |     price_volumes = analyze_volume_at_price(trades, order_book)
501 |     
502 |     # Sort prices for significant levels
503 |     sorted_levels = sorted(
504 |         [(price, data) for price, data in price_volumes.items()],
505 |         key=lambda x: x[1].total_value,
506 |         reverse=True
507 |     )
508 |     
509 |     # Get top 5 levels
510 |     significant_levels = [
511 |         {
512 |             "price": price,
513 |             "buy_volume": data.buy_volume,
514 |             "sell_volume": data.sell_volume,
515 |             "after_hour_buy": data.after_hour_buy,
516 |             "after_hour_sell": data.after_hour_sell,
517 |             "after_hour_unknown": data.after_hour_unknown,
518 |             "buy_value": data.buy_value,
519 |             "sell_value": data.sell_value,
520 |             "after_hour_buy_value": data.after_hour_buy_value,
521 |             "after_hour_sell_value": data.after_hour_sell_value,
522 |             "after_hour_unknown_value": data.after_hour_unknown_value,
523 |             "total_volume": data.total_volume,
524 |             "total_value": data.total_value,
525 |             "volume_imbalance": data.volume_imbalance,
526 |             "value_imbalance": data.value_imbalance,
527 |             "total_trades": data.total_trades,
528 |             "last_trade_time": data.last_trade_time
529 |         }
530 |         for price, data in sorted_levels[:5]
531 |     ]
532 |     
533 |     # Calculate trading summaries
534 |     buy_volume = sum(t.volume for t in trades if t.side == "bu")
535 |     sell_volume = sum(t.volume for t in trades if t.side == "sd")
536 |     after_hour_trades = [t for t in trades if t.side == "after-hour"]
537 |     
538 |     after_hour_buy = sum(t.volume for t in after_hour_trades if t.price >= order_book.ask_1.price)
539 |     after_hour_sell = sum(t.volume for t in after_hour_trades if t.price <= order_book.bid_1.price)
540 |     after_hour_unknown = sum(t.volume for t in after_hour_trades 
541 |                            if order_book.bid_1.price < t.price < order_book.ask_1.price)
542 |     
543 |     buy_value = sum(t.value for t in trades if t.side == "bu")
544 |     sell_value = sum(t.value for t in trades if t.side == "sd")
545 |     after_hour_buy_value = sum(t.value for t in after_hour_trades if t.price >= order_book.ask_1.price)
546 |     after_hour_sell_value = sum(t.value for t in after_hour_trades if t.price <= order_book.bid_1.price)
547 |     after_hour_unknown_value = sum(t.value for t in after_hour_trades 
548 |                                  if order_book.bid_1.price < t.price < order_book.ask_1.price)
549 | 
550 |     total_volume = buy_volume + sell_volume + after_hour_buy + after_hour_sell + after_hour_unknown
551 |     total_value = buy_value + sell_value + after_hour_buy_value + after_hour_sell_value + after_hour_unknown_value
552 | 
553 |     return {
554 |         "timestamp": order_book.timestamp,
555 |         "symbol": symbol,
556 |         "market_status": {
557 |             "current_price": order_book.match_price,
558 |             "bid_price": order_book.bid_1.price,
559 |             "bid_volume": order_book.bid_1.volume,
560 |             "ask_price": order_book.ask_1.price,
561 |             "ask_volume": order_book.ask_1.volume,
562 |             "spread": order_book.ask_1.price - order_book.bid_1.price
563 |         },
564 |         "volume_analysis": {
565 |             "significant_levels": significant_levels,
566 |             "current_bid_accumulated": price_volumes.get(order_book.bid_1.price, PriceVolumeData()).model_dump(),
567 |             "current_ask_accumulated": price_volumes.get(order_book.ask_1.price, PriceVolumeData()).model_dump()
568 |         },
569 |         "trading_summary": {
570 |             "period": f"last {TRADES_TO_FETCH} trades",
571 |             "total_trades": len(trades),
572 |             "volume": {
573 |                 "buy": buy_volume,
574 |                 "sell": sell_volume,
575 |                 "after_hour": {
576 |                     "buy": after_hour_buy,
577 |                     "sell": after_hour_sell,
578 |                     "unknown": after_hour_unknown,
579 |                     "total": after_hour_buy + after_hour_sell + after_hour_unknown
580 |                 },
581 |                 "total": total_volume,
582 |                 "buy_ratio": (buy_volume + after_hour_buy) / 
583 |                             (buy_volume + sell_volume + after_hour_buy + after_hour_sell) 
584 |                             if (buy_volume + sell_volume + after_hour_buy + after_hour_sell) > 0 else 0
585 |             },
586 |             "value": {
587 |                 "buy": buy_value,
588 |                 "sell": sell_value,
589 |                 "after_hour": {
590 |                     "buy": after_hour_buy_value,
591 |                     "sell": after_hour_sell_value,
592 |                     "unknown": after_hour_unknown_value,
593 |                     "total": after_hour_buy_value + after_hour_sell_value + after_hour_unknown_value
594 |                 },
595 |                 "total": total_value,
596 |                 "buy_ratio": (buy_value + after_hour_buy_value) / 
597 |                             (buy_value + sell_value + after_hour_buy_value + after_hour_sell_value)
598 |                             if (buy_value + sell_value + after_hour_buy_value + after_hour_sell_value) > 0 else 0
599 |             },
600 |             "unique_price_levels": len(price_volumes),
601 |             "average_price": total_value / total_volume if total_volume > 0 else 0
602 |         }
603 |     }
604 | 
605 | if __name__ == "__main__":
606 |     # Test the functions
607 |     symbol = "VIC"
608 |     fetch_and_store_stock_data(symbol)
609 |     print(analyze_stock_data(symbol))
610 | 
```