Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
6
Makefile
@ -1,13 +1,13 @@
|
|||||||
.PHONY: setup run test build-mac-selfcontained
|
.PHONY: setup run test build-mac-selfcontained
|
||||||
|
|
||||||
setup:
|
setup:
|
||||||
./run.sh --setup-only
|
./web/run.sh --setup-only
|
||||||
|
|
||||||
run:
|
run:
|
||||||
./run.sh
|
./web/run.sh
|
||||||
|
|
||||||
test: setup
|
test: setup
|
||||||
. .venv/bin/activate && pytest -q
|
. .venv/bin/activate && PYTHONPATH=web/src pytest -q web/src/tests
|
||||||
|
|
||||||
build-mac-selfcontained:
|
build-mac-selfcontained:
|
||||||
./scripts/build_selfcontained_mac_app.sh
|
./scripts/build_selfcontained_mac_app.sh
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
# ManeshTraderMac (Xcode Shell App)
|
|
||||||
|
|
||||||
This Xcode app is a native macOS shell around the existing Streamlit trading app.
|
|
||||||
|
|
||||||
## What It Does
|
|
||||||
- Auto-starts backend when the app launches
|
|
||||||
- Starts/stops a bundled backend executable from app resources
|
|
||||||
- Embeds the local UI using `WKWebView` at `http://127.0.0.1:8501`
|
|
||||||
- Keeps users inside the app window (no external browser required)
|
|
||||||
|
|
||||||
## Build Self-Contained App
|
|
||||||
1. From project root, build the embedded backend + macOS app:
|
|
||||||
- `./scripts/build_selfcontained_mac_app.sh`
|
|
||||||
2. Output app bundle:
|
|
||||||
- `dist-mac/<timestamp>/ManeshTraderMac.app`
|
|
||||||
3. Optional DMG packaging:
|
|
||||||
- `APP_BUNDLE_PATH="dist-mac/<timestamp>/ManeshTraderMac.app" ./scripts/create_installer_dmg.sh`
|
|
||||||
|
|
||||||
## Run in Xcode
|
|
||||||
1. Generate embedded backend binary:
|
|
||||||
- `./scripts/build_embedded_backend.sh`
|
|
||||||
2. Open `ManeshTraderMac/ManeshTraderMac.xcodeproj`
|
|
||||||
3. Build/Run the `ManeshTraderMac` scheme
|
|
||||||
4. The app auto-starts backend on launch
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Backend source of truth is the root web app (`app.py`, `manesh_trader/`).
|
|
||||||
- `scripts/build_embedded_backend.sh` compiles those files into:
|
|
||||||
- `ManeshTraderMac/ManeshTraderMac/EmbeddedBackend/ManeshTraderBackend`
|
|
||||||
- The Swift host launches that embedded binary directly from the installed app bundle.
|
|
||||||
157
README.md
@ -1,154 +1,61 @@
|
|||||||
# Real Bars vs Fake Bars Trend Analyzer (Streamlit)
|
# Real Bars vs Fake Bars Trend Analyzer
|
||||||
|
|
||||||
A Python web app that implements the **Real Bars vs Fake Bars** strategy:
|
A Python web app wrapped by a native macOS shell.
|
||||||
- Classifies each closed bar against the prior bar as `real_bull`, `real_bear`, or `fake`
|
|
||||||
- Ignores fake bars for trend sequence logic
|
|
||||||
- Starts/continues trend on 2+ same-direction real bars
|
|
||||||
- Reverses trend only after 2 consecutive real bars of the opposite direction
|
|
||||||
|
|
||||||
## Features
|
## Standardized Layout
|
||||||
- Symbol/timeframe/period inputs (Yahoo Finance via `yfinance`)
|
- `web/src/`: web backend + Streamlit app source
|
||||||
- Optional wick filtering (use previous candle body range)
|
- `mac/src/`: Xcode macOS shell app (`WKWebView` host)
|
||||||
- Optional volume filter (minimum Volume vs Volume-SMA)
|
- `scripts/`: build and packaging scripts
|
||||||
- Trend events table (starts and reversals)
|
- `docs/`: architecture and supporting docs
|
||||||
- Chart visualization (candles + real-bar markers + trend-colored volume)
|
- `skills/`: reusable project skills
|
||||||
- Market session gap handling for stocks via `Hide market-closed gaps (stocks)`:
|
|
||||||
- `1d`: removes weekend spacing
|
|
||||||
- intraday: removes weekends and closed-hour overnight spans
|
|
||||||
- Session alerts for newly detected trend events
|
|
||||||
- Export classified data to CSV
|
|
||||||
- Export chart to PDF (via Plotly + Kaleido)
|
|
||||||
- Lightweight backtest snapshot (signal at trend changes, scored by next-bar direction)
|
|
||||||
|
|
||||||
## Project Structure
|
## Web Source
|
||||||
- `app.py`: Streamlit entrypoint and UI orchestration
|
- `web/src/app.py`: Streamlit entrypoint and UI orchestration
|
||||||
- `manesh_trader/constants.py`: intervals, periods, trend labels
|
- `web/src/web_core/`: strategy/data/chart/export modules
|
||||||
- `manesh_trader/models.py`: data models (`TrendEvent`)
|
- `web/src/requirements.txt`: Python dependencies
|
||||||
- `manesh_trader/data.py`: market data fetch and live-bar safeguard
|
- `web/src/ONBOARDING.md`: in-app onboarding guide content
|
||||||
- `manesh_trader/strategy.py`: bar classification and trend detection logic
|
|
||||||
- `manesh_trader/analytics.py`: backtest snapshot metrics
|
|
||||||
- `manesh_trader/charting.py`: Plotly chart construction
|
|
||||||
- `manesh_trader/exporting.py`: export DataFrame transformation
|
|
||||||
- `requirements.txt`: dependencies
|
|
||||||
|
|
||||||
## Onboarding
|
## macOS Shell
|
||||||
See `ONBOARDING.md` for a complete step-by-step tutorial.
|
- Project location: `mac/src/` (`*.xcodeproj` auto-discovered by scripts)
|
||||||
|
- Uses `WKWebView` and launches embedded backend executable from app resources.
|
||||||
|
- No external browser required.
|
||||||
|
|
||||||
## Xcode macOS Shell
|
See `mac/src/README.md` for shell details.
|
||||||
An Xcode-based desktop shell lives in `ManeshTraderMac/`.
|
|
||||||
|
|
||||||
It provides a native window that starts/stops an embedded local backend executable and renders the UI in `WKWebView` (no external browser).
|
|
||||||
Backend source of truth stays at project root (`app.py`, `manesh_trader/`), and packaging compiles those files into `ManeshTraderBackend` inside the app bundle.
|
|
||||||
See `ManeshTraderMac/README.md` for setup and run steps.
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
### Easiest (one command)
|
### Quick start
|
||||||
```bash
|
```bash
|
||||||
./run.sh
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### macOS Double-Click Launcher
|
### Setup only
|
||||||
Double-click `Run ManeshTrader.command` in Finder.
|
|
||||||
|
|
||||||
If macOS blocks first launch, run once in Terminal:
|
|
||||||
```bash
|
```bash
|
||||||
xattr -d com.apple.quarantine "Run ManeshTrader.command"
|
./run.sh --setup-only
|
||||||
```
|
```
|
||||||
|
|
||||||
### macOS App Wrapper (`.app`)
|
### Tests
|
||||||
Build a native `.app` launcher:
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/create_mac_app.sh
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates `ManeshTrader.app` in the project root. You can double-click it or move it to `/Applications`.
|
## Build Self-Contained macOS App
|
||||||
|
|
||||||
Build a distributable installer DMG:
|
|
||||||
```bash
|
|
||||||
./scripts/create_installer_dmg.sh
|
|
||||||
```
|
|
||||||
Output: `ManeshTrader-YYYYMMDD-HHMMSS.dmg` in project root.
|
|
||||||
|
|
||||||
### Self-Contained macOS App (WKWebView + Embedded Backend)
|
|
||||||
Build an installable app where all backend logic ships inside `ManeshTraderMac.app`:
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build_selfcontained_mac_app.sh
|
./scripts/build_selfcontained_mac_app.sh
|
||||||
```
|
```
|
||||||
This outputs `dist-mac/<timestamp>/ManeshTraderMac.app`.
|
Output: `dist-mac/<timestamp>/<Scheme>.app`
|
||||||
|
|
||||||
Then package that app as DMG:
|
Package as DMG:
|
||||||
```bash
|
```bash
|
||||||
APP_BUNDLE_PATH="dist-mac/<timestamp>/ManeshTraderMac.app" ./scripts/create_installer_dmg.sh
|
APP_BUNDLE_PATH="dist-mac/<timestamp>/<Scheme>.app" ./scripts/create_installer_dmg.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: for easiest install on other Macs, use Apple code signing + notarization before sharing publicly.
|
## Optional Standalone Streamlit App
|
||||||
|
|
||||||
### Standalone Streamlit App (Alternative)
|
|
||||||
This older path builds a standalone Streamlit wrapper app:
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build_standalone_app.sh
|
./scripts/build_standalone_app.sh
|
||||||
```
|
```
|
||||||
Output: `dist-standalone/<timestamp>/dist/ManeshTrader.app`
|
Output: `dist-standalone/<timestamp>/dist/<RepoName>.app`
|
||||||
|
|
||||||
### Optional with Make
|
|
||||||
```bash
|
|
||||||
make run
|
|
||||||
make build-mac-selfcontained
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual
|
|
||||||
1. Create and activate a virtual environment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run the app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
streamlit run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Open the local URL printed by Streamlit (usually `http://localhost:8501`).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
1. Set `Symbol` (examples: `AAPL`, `MSFT`, `BTC-USD`, `ETH-USD`).
|
|
||||||
2. Choose `Timeframe` and `Period`.
|
|
||||||
3. Optionally adjust filters:
|
|
||||||
- `Use previous body range (ignore wicks)`
|
|
||||||
- `Enable volume filter`
|
|
||||||
- `Hide market-closed gaps (stocks)`
|
|
||||||
4. Review:
|
|
||||||
- Current trend status
|
|
||||||
- Real/fake bar counts
|
|
||||||
- Trend events and chart markers
|
|
||||||
5. Export results from the **Export** section.
|
|
||||||
|
|
||||||
## Strategy Logic Implemented
|
|
||||||
For closed bar `i` and previous closed bar `i-1`:
|
|
||||||
- Real Bullish Bar: `close[i] > high[i-1]` (or previous body high if wick filter enabled)
|
|
||||||
- Real Bearish Bar: `close[i] < low[i-1]` (or previous body low if wick filter enabled)
|
|
||||||
- Fake Bar: otherwise (inclusive range)
|
|
||||||
|
|
||||||
Trend state machine:
|
|
||||||
- Two consecutive real bullish bars => bullish trend active
|
|
||||||
- Two consecutive real bearish bars => bearish trend active
|
|
||||||
- Active trend persists until **two consecutive real bars in opposite direction**
|
|
||||||
- Single opposite real bar is treated as noise/fluke and does not reverse trend
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- The app is analysis-only; no order execution.
|
- Analysis-only app; no trade execution.
|
||||||
- Yahoo Finance interval availability depends on symbol and lookback window.
|
- Yahoo Finance interval availability depends on symbol/lookback.
|
||||||
- "Ignore potentially live last bar" is enabled by default to reduce incomplete-bar bias.
|
- For broad distribution, use code signing + notarization.
|
||||||
- Gap-hiding behavior is currently optimized for regular U.S. stock sessions; exchange holidays/half-days may still show occasional spacing.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
- If data fetch fails, verify symbol spelling and use a compatible interval/period combination.
|
|
||||||
- If PDF export is unavailable, ensure `kaleido` is installed in the same environment.
|
|
||||||
|
|||||||
5
Run App.command
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
./run.sh
|
||||||
@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
cd "$ROOT_DIR"
|
|
||||||
|
|
||||||
./run.sh
|
|
||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 792 B After Width: | Height: | Size: 792 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
@ -757,6 +757,9 @@ private final class TraderHost {
|
|||||||
private func bundledBackendExecutableURL() -> URL? {
|
private func bundledBackendExecutableURL() -> URL? {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
let candidates = [
|
let candidates = [
|
||||||
|
Bundle.main.url(forResource: "WebBackend", withExtension: nil, subdirectory: "EmbeddedBackend"),
|
||||||
|
Bundle.main.resourceURL?.appendingPathComponent("EmbeddedBackend/WebBackend"),
|
||||||
|
Bundle.main.url(forResource: "WebBackend", withExtension: nil),
|
||||||
Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil, subdirectory: "EmbeddedBackend"),
|
Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil, subdirectory: "EmbeddedBackend"),
|
||||||
Bundle.main.resourceURL?.appendingPathComponent("EmbeddedBackend/ManeshTraderBackend"),
|
Bundle.main.resourceURL?.appendingPathComponent("EmbeddedBackend/ManeshTraderBackend"),
|
||||||
Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil),
|
Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil),
|
||||||
@ -1,6 +1,6 @@
|
|||||||
This folder is populated by `scripts/build_embedded_backend.sh`.
|
This folder is populated by `scripts/build_embedded_backend.sh`.
|
||||||
|
|
||||||
- Output binary: `ManeshTraderBackend`
|
- Output binary: `WebBackend`
|
||||||
- Source of truth for backend code: project root (`app.py`, `manesh_trader/`)
|
- Source of truth for backend code: `web/src/` (`app.py`, `web_core/`)
|
||||||
|
|
||||||
Do not hand-edit generated binaries in this folder.
|
Do not hand-edit generated binaries in this folder.
|
||||||
BIN
mac/src/App/EmbeddedBackend/WebBackend
Executable file
@ -1,5 +1,4 @@
|
|||||||
//
|
//
|
||||||
///Users/mattbruce/Documents/Web/ManeshTrader/ManeshTraderMac/ManeshTraderMac/ContentView.swift ManeshTraderMacTests.swift
|
|
||||||
// ManeshTraderMacTests
|
// ManeshTraderMacTests
|
||||||
//
|
//
|
||||||
// Created by Matt Bruce on 2/13/26.
|
// Created by Matt Bruce on 2/13/26.
|
||||||
@ -32,17 +32,17 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */ = {
|
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = ManeshTraderMac;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
|
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = ManeshTraderMacTests;
|
path = AppTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */ = {
|
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = ManeshTraderMacUITests;
|
path = AppUITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
30
mac/src/README.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# macOS Shell App
|
||||||
|
|
||||||
|
Native macOS shell around the web app in `web/src/`.
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
- Starts/stops bundled backend executable from app resources
|
||||||
|
- Hosts UI in `WKWebView` at local `127.0.0.1` URL
|
||||||
|
- Keeps user inside app window (no external browser)
|
||||||
|
|
||||||
|
## Build Self-Contained App
|
||||||
|
From repo root:
|
||||||
|
1. Build embedded backend + macOS app:
|
||||||
|
- `./scripts/build_selfcontained_mac_app.sh`
|
||||||
|
2. Output app bundle:
|
||||||
|
- `dist-mac/<timestamp>/<Scheme>.app`
|
||||||
|
3. Optional DMG packaging:
|
||||||
|
- `APP_BUNDLE_PATH="dist-mac/<timestamp>/<Scheme>.app" ./scripts/create_installer_dmg.sh`
|
||||||
|
|
||||||
|
## Run In Xcode
|
||||||
|
From repo root:
|
||||||
|
1. Generate embedded backend binary:
|
||||||
|
- `./scripts/build_embedded_backend.sh`
|
||||||
|
2. Open project:
|
||||||
|
- `mac/src/*.xcodeproj`
|
||||||
|
3. Build/Run scheme:
|
||||||
|
- default: project name (or set `MAC_SCHEME` in scripts)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Web source of truth is `web/src/` (`web/src/app.py`, `web/src/web_core/`).
|
||||||
|
- Embedded backend binary is copied into the first `EmbeddedBackend/` folder discovered under the selected project directory.
|
||||||
30
run.sh
@ -1,32 +1,4 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/web/run.sh" "$@"
|
||||||
VENV_DIR="$ROOT_DIR/.venv"
|
|
||||||
SETUP_ONLY=false
|
|
||||||
|
|
||||||
if [[ "${1:-}" == "--setup-only" ]]; then
|
|
||||||
SETUP_ONLY=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -d "$VENV_DIR" ]]; then
|
|
||||||
echo "Creating virtual environment..."
|
|
||||||
python3 -m venv "$VENV_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
source "$VENV_DIR/bin/activate"
|
|
||||||
|
|
||||||
if [[ ! -f "$VENV_DIR/.deps_installed" ]] || [[ "$ROOT_DIR/requirements.txt" -nt "$VENV_DIR/.deps_installed" ]]; then
|
|
||||||
echo "Installing dependencies..."
|
|
||||||
pip install -r "$ROOT_DIR/requirements.txt"
|
|
||||||
touch "$VENV_DIR/.deps_installed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$SETUP_ONLY" == "true" ]]; then
|
|
||||||
echo "Setup complete."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Starting Streamlit app..."
|
|
||||||
exec streamlit run "$ROOT_DIR/app.py"
|
|
||||||
|
|||||||
@ -2,14 +2,43 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
WEB_SRC_DIR="$ROOT_DIR/web/src"
|
||||||
|
MAC_SRC_DIR="${MAC_SRC_DIR:-$ROOT_DIR/mac/src}"
|
||||||
PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
|
PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
|
||||||
LAUNCHER="$ROOT_DIR/backend_embedded_launcher.py"
|
LAUNCHER="$WEB_SRC_DIR/backend_embedded_launcher.py"
|
||||||
APP_NAME="ManeshTraderBackend"
|
BACKEND_BIN_NAME="${BACKEND_BIN_NAME:-WebBackend}"
|
||||||
BUILD_ROOT="$ROOT_DIR/dist-backend-build"
|
BUILD_ROOT="$ROOT_DIR/dist-backend-build"
|
||||||
DIST_PATH="$BUILD_ROOT/dist"
|
DIST_PATH="$BUILD_ROOT/dist"
|
||||||
WORK_PATH="$BUILD_ROOT/build"
|
WORK_PATH="$BUILD_ROOT/build"
|
||||||
SPEC_PATH="$BUILD_ROOT/spec"
|
SPEC_PATH="$BUILD_ROOT/spec"
|
||||||
TARGET_DIR="$ROOT_DIR/ManeshTraderMac/ManeshTraderMac/EmbeddedBackend"
|
PROJECT_PATH="${MAC_PROJECT_PATH:-}"
|
||||||
|
SCHEME="${MAC_SCHEME:-}"
|
||||||
|
TARGET_DIR="${EMBEDDED_BACKEND_DIR:-}"
|
||||||
|
|
||||||
|
discover_scheme() {
|
||||||
|
local project_path="$1"
|
||||||
|
python3 - "$project_path" <<'PY'
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
project = sys.argv[1]
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
["xcodebuild", "-list", "-project", project, "-json"],
|
||||||
|
text=True,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
payload = json.loads(output)
|
||||||
|
except Exception:
|
||||||
|
print("")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
project_data = payload.get("project", {})
|
||||||
|
schemes = project_data.get("schemes") or []
|
||||||
|
print(schemes[0] if schemes else "")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
if [[ ! -x "$PYTHON_BIN" ]]; then
|
if [[ ! -x "$PYTHON_BIN" ]]; then
|
||||||
echo "Missing virtual environment. Run ./run.sh --setup-only first." >&2
|
echo "Missing virtual environment. Run ./run.sh --setup-only first." >&2
|
||||||
@ -21,30 +50,74 @@ if [[ ! -f "$LAUNCHER" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$PROJECT_PATH" ]]; then
|
||||||
|
PROJECT_PATH="$(find "$MAC_SRC_DIR" -maxdepth 4 -name "*.xcodeproj" | sort | head -n 1)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$PROJECT_PATH" ]]; then
|
||||||
|
echo "No .xcodeproj found under: $MAC_SRC_DIR" >&2
|
||||||
|
echo "Set MAC_PROJECT_PATH to the project you want to build." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT_DIR="$(dirname "$PROJECT_PATH")"
|
||||||
|
if [[ -z "$SCHEME" ]]; then
|
||||||
|
SCHEME="$(discover_scheme "$PROJECT_PATH")"
|
||||||
|
fi
|
||||||
|
if [[ -z "$SCHEME" ]]; then
|
||||||
|
SCHEME="$(basename "$PROJECT_PATH" .xcodeproj)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$TARGET_DIR" ]]; then
|
||||||
|
DEFAULT_TARGET_DIR="$PROJECT_DIR/$SCHEME/EmbeddedBackend"
|
||||||
|
if [[ -d "$DEFAULT_TARGET_DIR" ]]; then
|
||||||
|
TARGET_DIR="$DEFAULT_TARGET_DIR"
|
||||||
|
else
|
||||||
|
TARGET_DIR="$(find "$PROJECT_DIR" -maxdepth 4 -type d -name EmbeddedBackend | sort | head -n 1)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$TARGET_DIR" ]]; then
|
||||||
|
TARGET_DIR="$PROJECT_DIR/$SCHEME/EmbeddedBackend"
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" "$TARGET_DIR"
|
mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" "$TARGET_DIR"
|
||||||
|
|
||||||
"$PYTHON_BIN" -m pip install -q pyinstaller
|
"$PYTHON_BIN" -m pip install -q pyinstaller
|
||||||
|
|
||||||
"$PYTHON_BIN" -m PyInstaller \
|
PYI_ARGS=(
|
||||||
--noconfirm \
|
--noconfirm
|
||||||
--clean \
|
--clean
|
||||||
--onefile \
|
--onefile
|
||||||
--name "$APP_NAME" \
|
--name "$BACKEND_BIN_NAME"
|
||||||
--distpath "$DIST_PATH" \
|
--distpath "$DIST_PATH"
|
||||||
--workpath "$WORK_PATH" \
|
--workpath "$WORK_PATH"
|
||||||
--specpath "$SPEC_PATH" \
|
--specpath "$SPEC_PATH"
|
||||||
--add-data "$ROOT_DIR/app.py:." \
|
--add-data "$WEB_SRC_DIR/app.py:."
|
||||||
--add-data "$ROOT_DIR/manesh_trader:manesh_trader" \
|
--collect-all streamlit
|
||||||
--add-data "$ROOT_DIR/ONBOARDING.md:." \
|
--collect-all streamlit_autorefresh
|
||||||
--collect-all streamlit \
|
--hidden-import yfinance
|
||||||
--collect-all streamlit_autorefresh \
|
--hidden-import pandas
|
||||||
--hidden-import yfinance \
|
--collect-all plotly
|
||||||
--hidden-import pandas \
|
--collect-all kaleido
|
||||||
--collect-all plotly \
|
)
|
||||||
--collect-all kaleido \
|
|
||||||
"$LAUNCHER"
|
|
||||||
|
|
||||||
cp "$DIST_PATH/$APP_NAME" "$TARGET_DIR/$APP_NAME"
|
for source_dir in "$WEB_SRC_DIR"/*; do
|
||||||
chmod +x "$TARGET_DIR/$APP_NAME"
|
source_name="$(basename "$source_dir")"
|
||||||
|
if [[ ! -d "$source_dir" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$source_name" == "tests" ]] || [[ "$source_name" == "__pycache__" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
PYI_ARGS+=(--add-data "$source_dir:$source_name")
|
||||||
|
done
|
||||||
|
|
||||||
echo "Embedded backend updated: $TARGET_DIR/$APP_NAME"
|
if [[ -f "$WEB_SRC_DIR/ONBOARDING.md" ]]; then
|
||||||
|
PYI_ARGS+=(--add-data "$WEB_SRC_DIR/ONBOARDING.md:.")
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$PYTHON_BIN" -m PyInstaller "${PYI_ARGS[@]}" "$LAUNCHER"
|
||||||
|
|
||||||
|
cp "$DIST_PATH/$BACKEND_BIN_NAME" "$TARGET_DIR/$BACKEND_BIN_NAME"
|
||||||
|
chmod +x "$TARGET_DIR/$BACKEND_BIN_NAME"
|
||||||
|
|
||||||
|
echo "Embedded backend updated: $TARGET_DIR/$BACKEND_BIN_NAME"
|
||||||
|
|||||||
@ -2,15 +2,56 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
PROJECT_PATH="$ROOT_DIR/ManeshTraderMac/ManeshTraderMac.xcodeproj"
|
MAC_SRC_DIR="${MAC_SRC_DIR:-$ROOT_DIR/mac/src}"
|
||||||
SCHEME="ManeshTraderMac"
|
PROJECT_PATH="${MAC_PROJECT_PATH:-}"
|
||||||
|
SCHEME="${MAC_SCHEME:-}"
|
||||||
CONFIGURATION="${CONFIGURATION:-Release}"
|
CONFIGURATION="${CONFIGURATION:-Release}"
|
||||||
DERIVED_DATA_PATH="$ROOT_DIR/dist-mac/derived-data"
|
DERIVED_DATA_PATH="$ROOT_DIR/dist-mac/derived-data"
|
||||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
OUTPUT_DIR="$ROOT_DIR/dist-mac/$TIMESTAMP"
|
OUTPUT_DIR="$ROOT_DIR/dist-mac/$TIMESTAMP"
|
||||||
|
|
||||||
|
discover_scheme() {
|
||||||
|
local project_path="$1"
|
||||||
|
python3 - "$project_path" <<'PY'
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
project = sys.argv[1]
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
["xcodebuild", "-list", "-project", project, "-json"],
|
||||||
|
text=True,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
payload = json.loads(output)
|
||||||
|
except Exception:
|
||||||
|
print("")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
project_data = payload.get("project", {})
|
||||||
|
schemes = project_data.get("schemes") or []
|
||||||
|
print(schemes[0] if schemes else "")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
"$ROOT_DIR/scripts/build_embedded_backend.sh"
|
"$ROOT_DIR/scripts/build_embedded_backend.sh"
|
||||||
|
|
||||||
|
if [[ -z "$PROJECT_PATH" ]]; then
|
||||||
|
PROJECT_PATH="$(find "$MAC_SRC_DIR" -maxdepth 4 -name "*.xcodeproj" | sort | head -n 1)"
|
||||||
|
fi
|
||||||
|
if [[ -z "$PROJECT_PATH" ]]; then
|
||||||
|
echo "No .xcodeproj found under: $MAC_SRC_DIR" >&2
|
||||||
|
echo "Set MAC_PROJECT_PATH to the project you want to build." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$SCHEME" ]]; then
|
||||||
|
SCHEME="$(discover_scheme "$PROJECT_PATH")"
|
||||||
|
fi
|
||||||
|
if [[ -z "$SCHEME" ]]; then
|
||||||
|
SCHEME="$(basename "$PROJECT_PATH" .xcodeproj)"
|
||||||
|
fi
|
||||||
|
|
||||||
xcodebuild \
|
xcodebuild \
|
||||||
-project "$PROJECT_PATH" \
|
-project "$PROJECT_PATH" \
|
||||||
-scheme "$SCHEME" \
|
-scheme "$SCHEME" \
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
WEB_SRC_DIR="$ROOT_DIR/web/src"
|
||||||
PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
|
PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
|
||||||
APP_NAME="ManeshTrader"
|
APP_NAME="${APP_NAME:-$(basename "$ROOT_DIR")}"
|
||||||
|
|
||||||
if [[ ! -x "$PYTHON_BIN" ]]; then
|
if [[ ! -x "$PYTHON_BIN" ]]; then
|
||||||
echo "Missing virtual environment. Run ./run.sh --setup-only first." >&2
|
echo "Missing virtual environment. Run ./run.sh --setup-only first." >&2
|
||||||
@ -20,22 +21,37 @@ SPEC_PATH="$OUT_ROOT/spec"
|
|||||||
|
|
||||||
mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH"
|
mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH"
|
||||||
|
|
||||||
"$PYTHON_BIN" -m PyInstaller \
|
PYI_ARGS=(
|
||||||
--noconfirm \
|
--noconfirm
|
||||||
--windowed \
|
--windowed
|
||||||
--name "$APP_NAME" \
|
--name "$APP_NAME"
|
||||||
--distpath "$DIST_PATH" \
|
--distpath "$DIST_PATH"
|
||||||
--workpath "$WORK_PATH" \
|
--workpath "$WORK_PATH"
|
||||||
--specpath "$SPEC_PATH" \
|
--specpath "$SPEC_PATH"
|
||||||
--add-data "$ROOT_DIR/app.py:." \
|
--add-data "$WEB_SRC_DIR/app.py:."
|
||||||
--add-data "$ROOT_DIR/manesh_trader:manesh_trader" \
|
--collect-all streamlit
|
||||||
--add-data "$ROOT_DIR/ONBOARDING.md:." \
|
--collect-all streamlit_autorefresh
|
||||||
--collect-all streamlit \
|
--hidden-import yfinance
|
||||||
--collect-all streamlit_autorefresh \
|
--hidden-import pandas
|
||||||
--hidden-import yfinance \
|
--collect-all plotly
|
||||||
--hidden-import pandas \
|
)
|
||||||
--collect-all plotly \
|
|
||||||
"$ROOT_DIR/desktop_launcher.py"
|
for source_dir in "$WEB_SRC_DIR"/*; do
|
||||||
|
source_name="$(basename "$source_dir")"
|
||||||
|
if [[ ! -d "$source_dir" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$source_name" == "tests" ]] || [[ "$source_name" == "__pycache__" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
PYI_ARGS+=(--add-data "$source_dir:$source_name")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -f "$WEB_SRC_DIR/ONBOARDING.md" ]]; then
|
||||||
|
PYI_ARGS+=(--add-data "$WEB_SRC_DIR/ONBOARDING.md:.")
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$PYTHON_BIN" -m PyInstaller "${PYI_ARGS[@]}" "$WEB_SRC_DIR/desktop_launcher.py"
|
||||||
|
|
||||||
APP_BUNDLE="$DIST_PATH/${APP_NAME}.app"
|
APP_BUNDLE="$DIST_PATH/${APP_NAME}.app"
|
||||||
if [[ ! -d "$APP_BUNDLE" ]]; then
|
if [[ ! -d "$APP_BUNDLE" ]]; then
|
||||||
|
|||||||
@ -2,12 +2,35 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
if [[ -d "$ROOT_DIR/ManeshTraderMac.app" ]]; then
|
|
||||||
APP_BUNDLE_DEFAULT="$ROOT_DIR/ManeshTraderMac.app"
|
find_latest_app_bundle() {
|
||||||
else
|
local candidate
|
||||||
APP_BUNDLE_DEFAULT="$ROOT_DIR/ManeshTrader.app"
|
|
||||||
|
candidate="$(find "$ROOT_DIR/dist-mac" -type d -name "*.app" 2>/dev/null | sort | tail -n 1 || true)"
|
||||||
|
if [[ -n "$candidate" ]]; then
|
||||||
|
echo "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
candidate="$(find "$ROOT_DIR/dist-standalone" -type d -name "*.app" 2>/dev/null | sort | tail -n 1 || true)"
|
||||||
|
if [[ -n "$candidate" ]]; then
|
||||||
|
echo "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
candidate="$(find "$ROOT_DIR" -maxdepth 1 -type d -name "*.app" | sort | head -n 1 || true)"
|
||||||
|
if [[ -n "$candidate" ]]; then
|
||||||
|
echo "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
APP_BUNDLE="${APP_BUNDLE_PATH:-}"
|
||||||
|
if [[ -z "$APP_BUNDLE" ]]; then
|
||||||
|
APP_BUNDLE="$(find_latest_app_bundle || true)"
|
||||||
fi
|
fi
|
||||||
APP_BUNDLE="${APP_BUNDLE_PATH:-$APP_BUNDLE_DEFAULT}"
|
|
||||||
|
|
||||||
if ! command -v create-dmg >/dev/null 2>&1; then
|
if ! command -v create-dmg >/dev/null 2>&1; then
|
||||||
echo "create-dmg not found. Install with: brew install create-dmg" >&2
|
echo "create-dmg not found. Install with: brew install create-dmg" >&2
|
||||||
@ -15,7 +38,7 @@ if ! command -v create-dmg >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -d "$APP_BUNDLE" ]]; then
|
if [[ ! -d "$APP_BUNDLE" ]]; then
|
||||||
echo "App bundle not found: $APP_BUNDLE" >&2
|
echo "App bundle not found." >&2
|
||||||
echo "Set APP_BUNDLE_PATH to a built .app bundle or build one first." >&2
|
echo "Set APP_BUNDLE_PATH to a built .app bundle or build one first." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
APP_NAME="ManeshTrader"
|
APP_NAME="${APP_NAME:-$(basename "$ROOT_DIR")}"
|
||||||
APP_PATH="$ROOT_DIR/${APP_NAME}.app"
|
APP_PATH="$ROOT_DIR/${APP_NAME}.app"
|
||||||
|
|
||||||
if ! command -v osacompile >/dev/null 2>&1; then
|
if ! command -v osacompile >/dev/null 2>&1; then
|
||||||
|
|||||||
33
web/run.sh
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
WEB_SRC_DIR="$ROOT_DIR/web/src"
|
||||||
|
VENV_DIR="$ROOT_DIR/.venv"
|
||||||
|
SETUP_ONLY=false
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--setup-only" ]]; then
|
||||||
|
SETUP_ONLY=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$VENV_DIR" ]]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
python3 -m venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
|
||||||
|
if [[ ! -f "$VENV_DIR/.deps_installed" ]] || [[ "$WEB_SRC_DIR/requirements.txt" -nt "$VENV_DIR/.deps_installed" ]]; then
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
pip install -r "$WEB_SRC_DIR/requirements.txt"
|
||||||
|
touch "$VENV_DIR/.deps_installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SETUP_ONLY" == "true" ]]; then
|
||||||
|
echo "Setup complete."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting Streamlit app..."
|
||||||
|
exec streamlit run "$WEB_SRC_DIR/app.py"
|
||||||
@ -9,14 +9,15 @@ import streamlit as st
|
|||||||
import yfinance as yf
|
import yfinance as yf
|
||||||
from streamlit_autorefresh import st_autorefresh
|
from streamlit_autorefresh import st_autorefresh
|
||||||
|
|
||||||
from manesh_trader.analytics import backtest_signals
|
from web_core.analytics import backtest_signals
|
||||||
from manesh_trader.charting import build_figure
|
from web_core.charting import build_figure
|
||||||
from manesh_trader.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS
|
from web_core.constants import INTERVAL_OPTIONS, PERIOD_OPTIONS
|
||||||
from manesh_trader.data import fetch_ohlc, maybe_drop_live_bar
|
from web_core.data import fetch_ohlc, maybe_drop_live_bar
|
||||||
from manesh_trader.exporting import df_for_export
|
from web_core.exporting import df_for_export
|
||||||
from manesh_trader.strategy import classify_bars, detect_trends
|
from web_core.strategy import classify_bars, detect_trends
|
||||||
|
|
||||||
SETTINGS_PATH = Path.home() / ".manesh_trader" / "settings.json"
|
SETTINGS_PATH = Path.home() / ".web_local_shell" / "settings.json"
|
||||||
|
LEGACY_SETTINGS_PATH = Path.home() / ".manesh_trader" / "settings.json"
|
||||||
|
|
||||||
|
|
||||||
def _clamp_int(value: Any, fallback: int, minimum: int, maximum: int) -> int:
|
def _clamp_int(value: Any, fallback: int, minimum: int, maximum: int) -> int:
|
||||||
@ -137,10 +138,11 @@ def normalize_web_settings(raw: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def load_web_settings() -> dict[str, Any]:
|
def load_web_settings() -> dict[str, Any]:
|
||||||
if not SETTINGS_PATH.exists():
|
source_path = SETTINGS_PATH if SETTINGS_PATH.exists() else LEGACY_SETTINGS_PATH
|
||||||
|
if not source_path.exists():
|
||||||
return normalize_web_settings(None)
|
return normalize_web_settings(None)
|
||||||
try:
|
try:
|
||||||
payload = json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
|
payload = json.loads(source_path.read_text(encoding="utf-8"))
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return normalize_web_settings(None)
|
return normalize_web_settings(None)
|
||||||
return normalize_web_settings(payload)
|
return normalize_web_settings(payload)
|
||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from manesh_trader.exporting import df_for_export
|
from web_core.exporting import df_for_export
|
||||||
|
|
||||||
|
|
||||||
def test_df_for_export_handles_named_datetime_index() -> None:
|
def test_df_for_export_handles_named_datetime_index() -> None:
|
||||||
@ -2,8 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from manesh_trader.constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL
|
from web_core.constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL
|
||||||
from manesh_trader.strategy import classify_bars, detect_trends
|
from web_core.strategy import classify_bars, detect_trends
|
||||||
|
|
||||||
|
|
||||||
def make_df(rows: list[dict[str, float]]) -> pd.DataFrame:
|
def make_df(rows: list[dict[str, float]]) -> pd.DataFrame:
|
||||||