# 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 |
```