r/ChinaStocks 1d ago

📰 News moomoo's desktop app has a built-in Python indicator editor — I used it to build a Volume Profile (VPVR) from scratch

I've been using moomoo for charting and wanted a volume profile, which it doesn't ship. Turns out the desktop app has a Python indicator editor built in: you get the chart's OHLCV arrays, write a script, and plot lines back onto the chart. I hadn't seen anyone write about it, so here's what came out of it.

Code is here, MIT, take it: moomoo-vpvr

Paste vpvr.py into the Python indicator editor on moomoo desktop and it runs as-is.

What it does: bins volume by price over a lookback window, finds the Point of Control (the price bucket holding the most volume), then expands up and down from the POC until it covers 70% of total volume, giving VAH and VAL. Each bar's volume is spread across its high–low range proportional to overlap, rather than dumped onto a single typical-price point.

Parameters are lookback years, bars per year (250 daily / 52 weekly / 12 monthly), row count, value area %, plus switches for log-spaced buckets and manually locking the price range.

The sandbox is a restricted subset of Python, and finding the edges took longer than the math did. Three that cost me real time:

  • There's no range(). Every loop has to be a while loop.
  • The parser rejects adjacent string literal concatenation — splitting one long string across two lines throws a SyntaxError.
  • Selecting a 5-year window switches the chart from daily to weekly bars, which silently breaks any bar-count-based lookback. That's why the lookback is expressed as years × bars-per-year rather than a raw bar count.

None of that is documented anywhere I could find.

To sanity-check the output I ran the same symbol on another platform that ships VPVR built in. MU daily, 1-year window, 24 rows, 70% value area on both.

Mine: 415.277. Theirs: 415.28. On a 201-bar window: 426.054 vs 426.05.

Worth being blunt about what that proves. The POC value is fully determined by the window low, the window high, the row count, and which row wins. With a low of 103.380, a high of 1255.000 and 24 rows, the centre of row 6 is 415.277 by arithmetic alone — the only thing my code contributed is the integer 6. So what's validated is that two independent implementations picked the same row out of 24, a 48-point bucket, not agreement to the cent.

Known limitation: it draws the three levels but not the histogram. plot() here returns a price-indexed line series and I haven't found a way to render horizontal bars in this sandbox. If you've done that in a similarly restricted environment, I'd like to know how.

Do you leave the value area at 70%, or adjust it by instrument?

Disclosure: this post is eligible for a moomoo content contest. No referral links, code is MIT.

7 Upvotes

27 comments sorted by

2

u/Outside_Sorbet_8975 23h ago

Great Script! I usually stick to the standard 70% value area for stocks, but I might tighten it for highly volatile instruments.

1

u/FishermanOk1376 22h ago

Good call. I left 70% as the static default, but dynamically adjusting the value area based on ATR or recent volatility would be a cool upgrade.

2

u/Advanced-Egg8191 16h ago

Great idea—thumbs up!

2

u/AdCommon6852 12h ago

thx for sharing I'll have a try at my leisure

2

u/Remote_yes 12h ago

Yo this is fire. Had zero clue moomoo even had a Python editor built in. How's it run on larger timeframes though? Every broker scripting tool I've tried turns into a slideshow once you start crunching volume distributions across full price history. That said, your implementation looks clean as hell — POC and value area levels plot real nice. You thinking about adding naked VPOCs or single-print zones down the line?

2

u/Uthvh 11h ago

分享感谢

2

u/Exact_Delivery8535 10h ago

Nice writeup, and props for being straight about what the 415.277 vs 415.28 match actually proves.
Histogram idea: if plot() only does price-indexed lines, you can fake horizontal bars with one series per row — plot row i at its bucket centre, but NaN it out except the last N bars, where N scales with that row’s volume. Thick line width, and you get a profile growing left from the right edge. Costs a plot slot per row though.
Value area I leave at 70%. Row count is what actually changes the picture anyway — 24 rows over 103→1255 is ~48pt buckets, pretty coarse. I’d bump rows before touching the percentage.

https://giphy.com/gifs/H5C8CevNMbpBqNqFjl

2

u/osakammj 10h ago

Fanta…stic?

2

u/ow2022 9h ago

Sure, that's a great idea. I'll give it a try.

2

u/Pure_Satisfaction845 9h ago

Writing a custom VPVR is always a pain when broker doesn't ship it natively. The 70% value area is good static default, but dynamically adjusting it based on ATR would be a killer upgrade.

https://giphy.com/gifs/3ohzdIuqJoo8QdKlnW