trendline fix and just
This commit is contained in:
parent
693d0873f8
commit
cb81672a37
5 changed files with 245 additions and 9 deletions
119
Justfile
Normal file
119
Justfile
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
set dotenv-load := true
|
||||
set shell := ["bash", "-euo", "pipefail", "-c"]
|
||||
|
||||
local_url := "http://localhost:" + env_var_or_default("PORT", "8010")
|
||||
production_url := env_var_or_default("URL", "https://chart.amow.com")
|
||||
|
||||
# List available project commands.
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Start the local stack in the background.
|
||||
up:
|
||||
docker compose up -d
|
||||
|
||||
# Build images and run the local stack in the foreground.
|
||||
dev:
|
||||
docker compose up --build
|
||||
|
||||
# Build or rebuild local images.
|
||||
build:
|
||||
docker compose build
|
||||
|
||||
# Recreate the API container after environment or dependency changes.
|
||||
restart:
|
||||
docker compose up -d --force-recreate api
|
||||
|
||||
# Stop and remove the local stack.
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
# Show local service state.
|
||||
ps:
|
||||
docker compose ps
|
||||
|
||||
# Validate and print the resolved Compose configuration.
|
||||
compose-config:
|
||||
docker compose config
|
||||
|
||||
# Follow service logs; e.g. `just logs api 20m`.
|
||||
logs service="api" since="30m":
|
||||
docker compose logs --follow --since "{{since}}" "{{service}}"
|
||||
|
||||
# Open a shell in the API container.
|
||||
shell:
|
||||
docker compose exec api sh
|
||||
|
||||
# Copy .env.example only when .env does not exist.
|
||||
env:
|
||||
@if [[ -e .env ]]; then echo ".env already exists"; else cp .env.example .env && echo "created .env"; fi
|
||||
|
||||
# Install local non-Docker development dependencies into .venv.
|
||||
venv:
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
|
||||
|
||||
# Run the app without Docker after `just venv`.
|
||||
serve:
|
||||
.venv/bin/uvicorn main:app --reload
|
||||
|
||||
# Run all backend tests, or one pytest path/selector.
|
||||
test selector="":
|
||||
@if [[ -n "{{selector}}" ]]; then docker compose exec -T api python -m pytest -q "{{selector}}"; else docker compose exec -T api python -m pytest -q; fi
|
||||
|
||||
# Run all browser tests, or files matching a filter such as `trendline`.
|
||||
e2e filter="":
|
||||
./bin/e2e "{{filter}}"
|
||||
|
||||
# Run complete backend and browser suites.
|
||||
test-all: test e2e
|
||||
|
||||
# Check patches for whitespace errors.
|
||||
check:
|
||||
git diff --check
|
||||
|
||||
# Require a clean committed worktree.
|
||||
clean:
|
||||
@if [[ -n "$(git status --porcelain)" ]]; then echo "worktree is not clean; commit intended changes first" >&2; exit 1; fi
|
||||
|
||||
# Run every required pre-deploy check.
|
||||
predeploy: check test-all
|
||||
|
||||
# Capture the local chart through the Playwright service.
|
||||
screenshot file="chart.png":
|
||||
docker compose exec -T playwright playwright screenshot --lang en-US --wait-for-timeout 5000 http://api:8000 "/artifacts/{{file}}"
|
||||
|
||||
# Show local API status.
|
||||
status:
|
||||
curl -fsS "{{local_url}}/api/status"
|
||||
|
||||
# Replay Yahoo history through the alert calibration sweep.
|
||||
calibrate:
|
||||
docker compose exec -T api python -m scripts.calibrate_alerts
|
||||
|
||||
# Validate existing Schwab credentials, or print the authorization URL.
|
||||
schwab-check redirect_url="":
|
||||
@if [[ -n "{{redirect_url}}" ]]; then docker compose exec -T api python -m scripts.check_schwab --redirect-url "{{redirect_url}}"; else docker compose exec -T api python -m scripts.check_schwab; fi
|
||||
|
||||
# Confirm Schwab stream messages for a duration and symbol.
|
||||
stream seconds="60" symbol="/ES":
|
||||
docker compose exec -T api python -m scripts.check_stream "{{seconds}}" "{{symbol}}"
|
||||
|
||||
# Wait until production serves local HEAD.
|
||||
wait-deploy url=production_url timeout="300":
|
||||
URL="{{url}}" TIMEOUT="{{timeout}}" ./bin/wait-deploy
|
||||
|
||||
# Check public production health and deployed version.
|
||||
smoke url=production_url:
|
||||
curl -fsS "{{url}}/api/health"
|
||||
curl -fsS "{{url}}/api/version"
|
||||
|
||||
# Check authenticated production status using TOKEN from the environment.
|
||||
production-status url=production_url:
|
||||
@if [[ -z "${TOKEN:-}" ]]; then echo "TOKEN is required" >&2; exit 1; fi; curl -fsS -H "X-Chart-Token: $TOKEN" "{{url}}/api/status"
|
||||
|
||||
# Require a clean tree, run all tests, push main, wait, and smoke test.
|
||||
deploy: clean predeploy
|
||||
git push origin main
|
||||
./bin/wait-deploy
|
||||
just smoke
|
||||
69
README.md
69
README.md
|
|
@ -19,9 +19,11 @@ housekeeping after it.
|
|||
## Local development
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
just dev
|
||||
```
|
||||
|
||||
Equivalent raw command: `docker compose up --build`.
|
||||
|
||||
Then open <http://localhost:8010>. Override the host port with, for example,
|
||||
`PORT=8020 docker compose up`. The port binds to all host interfaces, so another
|
||||
machine can connect at `http://HOST_IP:8010`. The source tree is bind-mounted and uvicorn
|
||||
|
|
@ -85,17 +87,63 @@ VWAP) and 16s (prior-day levels), so `20`, `24` and `28` behave identically and
|
|||
falls to zero. Cooldown is the finer knob. Revisit both as more varied tapes are
|
||||
recorded — six sessions is not much, and one of them dominates the totals.
|
||||
|
||||
## Just commands
|
||||
|
||||
The root [`Justfile`](Justfile) is the supported command interface. Install
|
||||
[`just`](https://github.com/casey/just), then list every recipe with:
|
||||
|
||||
```bash
|
||||
just
|
||||
# or: just --list
|
||||
```
|
||||
|
||||
Common commands:
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `just up` | Start the local Compose stack in the background |
|
||||
| `just dev` | Build and run the stack in the foreground |
|
||||
| `just down` | Stop the local stack |
|
||||
| `just restart` | Recreate the API container after environment changes |
|
||||
| `just ps` | Show container state |
|
||||
| `just logs` | Follow API logs from the last 30 minutes |
|
||||
| `just logs playwright 10m` | Follow another service with a custom lookback |
|
||||
| `just shell` | Open a shell in the API container |
|
||||
| `just test` | Run the complete backend suite |
|
||||
| `just test tests/test_aggregator.py` | Run a backend path or pytest selector |
|
||||
| `just e2e` | Run the complete browser suite |
|
||||
| `just e2e trendline` | Run matching browser test files |
|
||||
| `just test-all` | Run complete backend and browser suites |
|
||||
| `just clean` | Require a clean committed worktree |
|
||||
| `just predeploy` | Run `git diff --check` and every test |
|
||||
| `just deploy` | Run predeploy checks, require a clean tree, push `main`, wait, and smoke test |
|
||||
| `just screenshot` | Save the local chart to `artifacts/playwright/chart.png` |
|
||||
| `just status` | Read local API status |
|
||||
| `just smoke` | Check production health and deployed version |
|
||||
| `just production-status` | Read authenticated production status using `$TOKEN` |
|
||||
| `just calibrate` | Run alert calibration in the API container |
|
||||
| `just schwab-check` | Validate Schwab credentials or start authorization |
|
||||
| `just stream 60 /ES` | Probe the Schwab stream |
|
||||
|
||||
`just env` creates `.env` from `.env.example` only when it is absent. `just
|
||||
venv` and `just serve` provide the non-Docker setup. Recipes accept `PORT`,
|
||||
`URL`, `TIMEOUT`, `TOKEN`, and `E2E_URL` through the environment where relevant.
|
||||
|
||||
## Testing
|
||||
|
||||
Run the complete backend suite in the same container environment as the app:
|
||||
|
||||
```bash
|
||||
just test
|
||||
# equivalent:
|
||||
docker exec chart-api-1 sh -c "cd /app && python -m pytest -q"
|
||||
```
|
||||
|
||||
Pass a path or pytest selector for a focused run:
|
||||
|
||||
```bash
|
||||
just test 'tests/test_aggregator.py::test_closed_yahoo_hours_form_the_right_cme_daily_bar_across_1800_et'
|
||||
# equivalent:
|
||||
docker exec chart-api-1 sh -c \
|
||||
"cd /app && python -m pytest -q tests/test_aggregator.py::test_closed_yahoo_hours_form_the_right_cme_daily_bar_across_1800_et"
|
||||
```
|
||||
|
|
@ -103,6 +151,10 @@ docker exec chart-api-1 sh -c \
|
|||
Run every browser test against the local Compose stack, or filter by filename:
|
||||
|
||||
```bash
|
||||
just e2e
|
||||
just e2e trendline
|
||||
just e2e preferences
|
||||
# equivalents:
|
||||
./bin/e2e
|
||||
./bin/e2e trendline
|
||||
./bin/e2e preferences
|
||||
|
|
@ -125,13 +177,15 @@ python -m pytest -q
|
|||
Recommended pre-deploy and deploy verification:
|
||||
|
||||
```bash
|
||||
docker exec chart-api-1 sh -c "cd /app && python -m pytest -q"
|
||||
./bin/e2e
|
||||
git push && bin/wait-deploy
|
||||
curl -fsS https://chart.amow.com/api/health
|
||||
curl -fsS https://chart.amow.com/api/version
|
||||
just predeploy
|
||||
# commit the verified changes, then:
|
||||
just deploy
|
||||
```
|
||||
|
||||
`just deploy` runs both complete test suites again, refuses a dirty worktree,
|
||||
pushes `main`, waits for production to serve local `HEAD`, and checks health and
|
||||
version. The raw commands remain available when diagnosing an individual step.
|
||||
|
||||
For an authenticated production status smoke test, use the chart token without
|
||||
printing it:
|
||||
|
||||
|
|
@ -147,6 +201,7 @@ curl -fsS -H "X-Chart-Token: $TOKEN" https://chart.amow.com/api/status | jq .
|
|||
| `app/` | Market sources, aggregation, analysis, alerts, and API |
|
||||
| `static/` | `index.html`, `app.js`, `style.css` — Vue 3 loaded from unpkg |
|
||||
| `requirements.txt` | Python deps |
|
||||
| `Justfile` | Supported development, test, and deployment commands |
|
||||
| `Procfile` | Start command; **nixpacks needs this** or the deploy has nothing to run |
|
||||
| `bin/wait-deploy` | Blocks until the live site serves your latest commit |
|
||||
| `Dockerfile.dev`, `docker-compose.yml` | Local dev only — production does not use them |
|
||||
|
|
@ -163,7 +218,7 @@ serving for the whole build.
|
|||
To know when your commit is actually live, rather than guessing:
|
||||
|
||||
```bash
|
||||
git push && bin/wait-deploy
|
||||
git push && just wait-deploy
|
||||
```
|
||||
|
||||
It polls `/api/version` (which returns the `SOURCE_COMMIT` Coolify bakes into
|
||||
|
|
|
|||
|
|
@ -1127,6 +1127,14 @@ selection path, including duplication, runs the same focus method. Browser
|
|||
coverage checks two rows simultaneously, requires the newest one to own the
|
||||
focus and hit polyline, and starts a real pointer drag on that line.
|
||||
|
||||
A daily line exposed a second selection failure after that fix. Its source
|
||||
geometry and endpoint handles were valid, but the whole-line hit polyline sampled
|
||||
through visible future indexes. Non-session daily timestamps return no canonical
|
||||
price; the renderer treated one null point as grounds to hide the entire hit
|
||||
target. Hit polylines now omit unpriceable future points and remain active when
|
||||
at least two valid points survive. A daily-chart browser regression selects a
|
||||
real 1d line from the list and persists a body drag.
|
||||
|
||||
### 2026-08-15 — 1m zoom-out died at ~1am because the socket sent 1,000 bars
|
||||
|
||||
Compressing the 1m time scale stopped around 01:00, then looked empty.
|
||||
|
|
|
|||
|
|
@ -2680,8 +2680,11 @@ class ConfluenceChart {
|
|||
})).map(point => ({
|
||||
x: point.x,
|
||||
y: point.y == null ? null : this.candles.priceToCoordinate(point.y),
|
||||
}));
|
||||
if (hitPoints.length < 2 || hitPoints.some(point => point.x == null || point.y == null)) {
|
||||
})).filter(point => point.x != null && point.y != null);
|
||||
// Daily future slots can include non-session dates that canonical source
|
||||
// geometry refuses to price. Omit those points; one null must not disable
|
||||
// selection and body dragging for the entire otherwise-valid line.
|
||||
if (hitPoints.length < 2) {
|
||||
this.lineHitTarget.setAttribute('hidden', '');
|
||||
} else {
|
||||
this.lineHitTarget.removeAttribute('hidden');
|
||||
|
|
|
|||
|
|
@ -116,6 +116,57 @@ test('the newest checked trendline stays active and movable during bulk selectio
|
|||
});
|
||||
});
|
||||
|
||||
test('a daily trendline selected from the list can be dragged by its body',
|
||||
{ timeout: 180000 }, async () => {
|
||||
await withChart(async page => {
|
||||
const created = await page.evaluate(async () => {
|
||||
const bars = (await (await fetch('/api/bars?tf=1d&limit=30')).json()).bars;
|
||||
const first = bars.at(-24);
|
||||
const second = bars.at(-14);
|
||||
return await (await fetch('/api/lines', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tf: '1d', side: 'support', anchor_t: first.t, anchor_p: first.l,
|
||||
end_t: second.t, end_p: second.l + 20, note: `daily drag ${Date.now()}`,
|
||||
}),
|
||||
})).json();
|
||||
});
|
||||
await page.click('.timeframes button:text-is("1d")');
|
||||
await page.waitForFunction(() => window.__chart.bars[0]?.tf === '1d');
|
||||
const row = page.locator(`.trendline-row[data-drawing-id="${created.id}"]`);
|
||||
await row.locator('.line-select').check();
|
||||
await page.waitForFunction(id => window.__chart.selectedLineId === id
|
||||
&& !window.__chart.lineHitTarget.hasAttribute('hidden'), created.id);
|
||||
const original = await page.evaluate(id => {
|
||||
const chart = window.__chart;
|
||||
const line = chart.levels.find(item => item.id === id);
|
||||
const hit = chart.lineHitTarget;
|
||||
const plot = chart.plotCanvas().getBoundingClientRect();
|
||||
const spacing = chart.coordinateAtIndex(chart.bars.length - 1)
|
||||
- chart.coordinateAtIndex(chart.bars.length - 2);
|
||||
return {
|
||||
anchor_t: line.anchor_t,
|
||||
anchor_p: line.anchor_p,
|
||||
x: plot.left + (Number(hit.getAttribute('x1')) + Number(hit.getAttribute('x2'))) / 2,
|
||||
y: plot.top + (Number(hit.getAttribute('y1')) + Number(hit.getAttribute('y2'))) / 2,
|
||||
dx: spacing * 2,
|
||||
};
|
||||
}, created.id);
|
||||
|
||||
await page.mouse.move(original.x, original.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(original.x + original.dx, original.y + 20, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
await page.waitForFunction(([id, before]) => {
|
||||
const line = window.__chart.levels.find(item => item.id === id);
|
||||
return line && (line.anchor_t !== before.anchor_t || line.anchor_p !== before.anchor_p);
|
||||
}, [created.id, original]);
|
||||
assert.equal(await page.evaluate(() => window.__chart.selectedLineId), created.id,
|
||||
'daily body drag cleared the active selection');
|
||||
assertNoPageErrors(page, assert);
|
||||
});
|
||||
});
|
||||
|
||||
test('editing a drawing name with Backspace or Delete cannot delete the drawing',
|
||||
{
|
||||
timeout: 180000,
|
||||
|
|
|
|||
Loading…
Reference in a new issue