fib levels

This commit is contained in:
Chris Amow 2026-08-19 09:09:47 +00:00
parent 08cff3ce53
commit 58a64c1eeb
8 changed files with 240 additions and 11 deletions

View file

@ -54,6 +54,10 @@ class ManualLine:
def is_comment(self) -> bool: def is_comment(self) -> bool:
return self.drawing_kind in {"comment", "symbol"} return self.drawing_kind in {"comment", "symbol"}
@property
def is_overlay(self) -> bool:
return self.drawing_kind in {"comment", "symbol", "fibonacci"}
@property @property
def horizontal(self) -> bool: def horizontal(self) -> bool:
"""A typed price level rather than a drawn trendline.""" """A typed price level rather than a drawn trendline."""
@ -194,7 +198,7 @@ class ManualLineStore:
put it in a confluence cluster and fire a push notification about a put it in a confluence cluster and fire a push notification about a
piece of text. piece of text.
""" """
return [line.to_level() for line in self.lines.values() if not line.is_comment] return [line.to_level() for line in self.lines.values() if not line.is_overlay]
def drawings(self) -> list[ManualLine]: def drawings(self) -> list[ManualLine]:
"""Everything drawn, comments included, newest number last.""" """Everything drawn, comments included, newest number last."""

View file

@ -82,6 +82,7 @@ class LineCreate(BaseModel):
line_width: int = Field(2, ge=1, le=9) line_width: int = Field(2, ge=1, le=9)
cutoff_t: int | None = None cutoff_t: int | None = None
armed: bool = True armed: bool = True
kind: str = ""
class PriceAlertCreate(BaseModel): class PriceAlertCreate(BaseModel):
@ -214,16 +215,17 @@ def put_ma_alerts(request: Request, payload: dict):
@router.post("/lines", status_code=201) @router.post("/lines", status_code=201)
def create_line(request: Request, payload: LineCreate): def create_line(request: Request, payload: LineCreate):
if payload.end_t == payload.anchor_t: if payload.end_t == payload.anchor_t and payload.kind != "fibonacci":
raise HTTPException(400, "Line endpoints must have different times") raise HTTPException(400, "Line endpoints must have different times")
last_t = payload.end_t if payload.end_t != payload.anchor_t else payload.anchor_t + 1
line = ManualLine( line = ManualLine(
id=f"ml_{uuid.uuid4().hex}", id=f"ml_{uuid.uuid4().hex}",
tf=payload.tf, tf=payload.tf,
side=payload.side, side=payload.side,
anchor_t=payload.anchor_t, anchor_t=payload.anchor_t,
anchor_p=payload.anchor_p, anchor_p=payload.anchor_p,
slope=(payload.end_p - payload.anchor_p) / (payload.end_t - payload.anchor_t), slope=(payload.end_p - payload.anchor_p) / (last_t - payload.anchor_t),
last_t=payload.end_t, last_t=last_t,
created_at=int(time.time()), created_at=int(time.time()),
note=payload.note, note=payload.note,
hidden=payload.hidden, hidden=payload.hidden,
@ -231,10 +233,13 @@ def create_line(request: Request, payload: LineCreate):
line_width=payload.line_width, line_width=payload.line_width,
cutoff_t=payload.cutoff_t, cutoff_t=payload.cutoff_t,
armed=payload.armed, armed=payload.armed,
kind=payload.kind,
) )
runtime = request.app.state.runtime runtime = request.app.state.runtime
line = runtime.manual_lines.add(line) line = runtime.manual_lines.add(line)
runtime.rebuild_levels() runtime.rebuild_levels()
if line.is_overlay:
return {**line.to_dict(), "kind": line.drawing_kind}
return positioned_level(runtime, line.id).to_dict() return positioned_level(runtime, line.id).to_dict()
@ -388,7 +393,7 @@ def patch_line(request: Request, line_id: str, payload: LinePatch):
request.app.state.runtime.rebuild_levels() request.app.state.runtime.rebuild_levels()
# A comment has no level form — returning one would hand the caller a shape # A comment has no level form — returning one would hand the caller a shape
# that looks like something the confluence engine tracks. # that looks like something the confluence engine tracks.
return line.to_dict() if line.is_comment else positioned_level( return line.to_dict() if line.is_overlay else positioned_level(
request.app.state.runtime, line.id, request.app.state.runtime, line.id,
).to_dict() ).to_dict()

View file

@ -1207,3 +1207,12 @@ past `bars[-1].t` asked `timeToCoordinate` for that bar. Placement and
rendering now share the same future-slot map trendlines already use rendering now share the same future-slot map trendlines already use
(`indexAt` / `timeAtIndex` / `coordinateAtTime`). A mark in the right-hand (`indexAt` / `timeAtIndex` / `coordinateAtTime`). A mark in the right-hand
whitespace stays there. whitespace stays there.
### 2026-08-18 — Fibonacci retrace
A Fibonacci tool under Tools: drag from one swing to the other, same
two-click/drag gesture as a trendline. Stored as `kind=fibonacci` so it
shares numbering, hide, delete and colour with other drawings, but it is
not a confluence level. The chart draws native price lines at 0, 23.6,
38.2, 50, 61.8, 78.6 and 100 between the two prices. 0% is the first
click.

View file

@ -130,6 +130,7 @@ createApp({
// Comments are not levels, so they never arrive over the levels socket and // Comments are not levels, so they never arrive over the levels socket and
// are fetched and refreshed explicitly. // are fetched and refreshed explicitly.
const comments = ref([]); const comments = ref([]);
const fibs = ref([]);
const drawingFilter = ref(''); const drawingFilter = ref('');
const drawingKind = ref('all'); const drawingKind = ref('all');
const maAlerts = ref({ '1d': [] }); const maAlerts = ref({ '1d': [] });
@ -281,7 +282,15 @@ createApp({
hidden: comment.hidden, hidden: comment.hidden,
comment, comment,
})); }));
return [...lines, ...notes].sort((a, b) => a.number - b.number); const retraces = fibs.value.map(fib => ({
id: fib.id,
number: fib.number,
kind: 'fibonacci',
label: fib.note || 'fib',
hidden: fib.hidden,
line: fib,
}));
return [...lines, ...notes, ...retraces].sort((a, b) => a.number - b.number);
}); });
const filteredDrawings = computed(() => { const filteredDrawings = computed(() => {
@ -691,6 +700,10 @@ createApp({
await createSymbol(result); await createSymbol(result);
return; return;
} }
if (result.tool === 'fibonacci') {
await createFibonacci(result.start, result.end);
return;
}
if (result.tool === 'level') { if (result.tool === 'level') {
alertPrice.value = result.price; alertPrice.value = result.price;
await addPriceAlert(); await addPriceAlert();
@ -704,7 +717,10 @@ createApp({
if (!response.ok) return; if (!response.ok) return;
const payload = await response.json(); const payload = await response.json();
comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind)); comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind));
fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci');
syncComments(); syncComments();
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
? fibs.value.filter(item => !item.hidden) : []);
} }
function syncComments() { function syncComments() {
@ -808,6 +824,27 @@ createApp({
syncVisibleLevels(); syncVisibleLevels();
} }
async function createFibonacci(start, end) {
const side = start.p <= end.p ? 'support' : 'resistance';
const response = await apiFetch('/api/lines', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tf: timeframe.value, side,
anchor_t: start.t, anchor_p: start.p, end_t: end.t, end_p: end.p,
note: drawName.value, color: drawColor.value, line_width: drawWidth.value,
kind: 'fibonacci',
}),
});
if (!response.ok) {
console.error(`Unable to save fibonacci: HTTP ${response.status}`);
return;
}
await refreshComments();
const saved = await response.json();
selectedDrawing.value = saved.id;
selectedDrawings.value = [saved.id];
}
async function createTrendline(first, second) { async function createTrendline(first, second) {
const [start, end] = [first, second].sort((a, b) => a.t - b.t); const [start, end] = [first, second].sort((a, b) => a.t - b.t);
const side = start.snappedSide || end.snappedSide || drawSide.value; const side = start.snappedSide || end.snappedSide || drawSide.value;
@ -980,7 +1017,7 @@ createApp({
const saved = await response.json(); const saved = await response.json();
// A comment is not a level and must not be pushed into the level list, // A comment is not a level and must not be pushed into the level list,
// where it would be drawn as a line. // where it would be drawn as a line.
if (['comment', 'symbol'].includes(line.kind)) { await refreshComments(); return; } if (['comment', 'symbol', 'fibonacci'].includes(line.kind)) { await refreshComments(); return; }
levels.value = levels.value.map(level => level.id === line.id ? saved : level); levels.value = levels.value.map(level => level.id === line.id ? saved : level);
syncVisibleLevels(); syncVisibleLevels();
} }
@ -1002,6 +1039,7 @@ createApp({
}); });
if (!response.ok) { console.error(`Unable to move line: HTTP ${response.status}`); return; } if (!response.ok) { console.error(`Unable to move line: HTTP ${response.status}`); return; }
const saved = await response.json(); const saved = await response.json();
if (line.kind === 'fibonacci') { await refreshComments(); return; }
levels.value = levels.value.map(level => level.id === line.id ? saved : level); levels.value = levels.value.map(level => level.id === line.id ? saved : level);
syncVisibleLevels(); syncVisibleLevels();
} }
@ -1197,6 +1235,8 @@ createApp({
syncVisibleLevels(); syncVisibleLevels();
syncComments(); syncComments();
if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth); if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth);
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
? fibs.value.filter(item => !item.hidden) : []);
sendPrefs(); sendPrefs();
}, { deep: true }); }, { deep: true });
@ -1233,7 +1273,9 @@ createApp({
watch(selectedDrawing, id => { watch(selectedDrawing, id => {
if (chartApi) { if (chartApi) {
chartApi.setSelectedDrawing(id); chartApi.setSelectedDrawing(id);
chartApi.setSelectedLine(levels.value.some(level => level.id === id) ? id : null); chartApi.setSelectedLine(
levels.value.some(level => level.id === id) || fibs.value.some(fib => fib.id === id) ? id : null,
);
} }
revealSelectedDrawing(id); revealSelectedDrawing(id);
}); });

View file

@ -134,6 +134,9 @@ class ConfluenceChart {
this.overlayLayer = null; this.overlayLayer = null;
this.rthPrimitive = null; this.rthPrimitive = null;
this.rthEnabled = true; this.rthEnabled = true;
this.fibs = [];
this.fibLines = new Map();
this.fibPreview = null;
this.futureSpace = null; this.futureSpace = null;
this.snapDot = null; this.snapDot = null;
this.snapLabel = null; this.snapLabel = null;
@ -373,6 +376,9 @@ class ConfluenceChart {
this.previewLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); this.previewLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
this.previewLine.setAttribute('hidden', ''); this.previewLine.setAttribute('hidden', '');
preview.appendChild(this.previewLine); preview.appendChild(this.previewLine);
this.fibPreview = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.fibPreview.setAttribute('hidden', '');
preview.appendChild(this.fibPreview);
this.overlayLayer.appendChild(preview); this.overlayLayer.appendChild(preview);
this.lineBridgeLayer = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.lineBridgeLayer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.lineBridgeLayer.classList.add('chart-line-bridges'); this.lineBridgeLayer.classList.add('chart-line-bridges');
@ -684,6 +690,49 @@ class ConfluenceChart {
} }
} }
setFibs(fibs) {
this.fibs = fibs || [];
this.syncFibLines();
this.scheduleOverlays(true);
}
static fibPrices(fib) {
const start = Number(fib.anchor_p);
const end = start + Number(fib.slope || 0) * (Number(fib.last_t) - Number(fib.anchor_t));
return ConfluenceChart.FIB_RATIOS.map(item => ({
...item,
price: start + (end - start) * item.r,
}));
}
syncFibLines() {
if (!this.candles) return;
const wanted = new Set();
for (const fib of this.fibs) {
if (fib.hidden) continue;
for (const level of ConfluenceChart.fibPrices(fib)) {
const id = `${fib.id}:${level.r}`;
wanted.add(id);
const options = {
price: level.price,
color: fib.color || '#c8992f',
lineWidth: level.r === 0 || level.r === 0.618 || level.r === 1 ? 2 : 1,
lineStyle: level.r === 0.618 ? LightweightCharts.LineStyle.Solid : LightweightCharts.LineStyle.Dashed,
axisLabelVisible: true,
title: `${level.label} ${level.price.toFixed(2)}`,
};
const existing = this.fibLines.get(id);
if (existing) existing.applyOptions(options);
else this.fibLines.set(id, this.candles.createPriceLine(options));
}
}
for (const [id, line] of this.fibLines) {
if (wanted.has(id)) continue;
this.candles.removePriceLine(line);
this.fibLines.delete(id);
}
}
setComments(comments) { setComments(comments) {
this.comments = comments; this.comments = comments;
this.scheduleOverlays(true); this.scheduleOverlays(true);
@ -1602,6 +1651,10 @@ class ConfluenceChart {
} }
renderPending(a, b) { renderPending(a, b) {
if (this.armedTool === 'fibonacci') {
this.setFibPreview(a, b);
return;
}
if (a.t != null && b.t != null) this.setLinePreview(a, b, '#65b7cf', 2); if (a.t != null && b.t != null) this.setLinePreview(a, b, '#65b7cf', 2);
} }
@ -1699,7 +1752,9 @@ class ConfluenceChart {
const a = dragged ? this.snapPoint(start) : this.pendingAnchor; const a = dragged ? this.snapPoint(start) : this.pendingAnchor;
this.pendingAnchor = null; this.pendingAnchor = null;
this.clearLinePreview(); this.clearLinePreview();
if (a.t == null || b.t == null || a.t === b.t) return; if (a.t == null || b.t == null) return;
if (tool !== 'fibonacci' && a.t === b.t) return;
if (tool === 'fibonacci' && a.p === b.p) return;
this.onToolComplete?.({ tool, start: a, end: b }); this.onToolComplete?.({ tool, start: a, end: b });
} }
@ -1800,6 +1855,10 @@ class ConfluenceChart {
} }
const a = this.snapPoint(start); const a = this.snapPoint(start);
const b = this.snapPoint(end); const b = this.snapPoint(end);
if (this.armedTool === 'fibonacci') {
this.setFibPreview(a, b);
return;
}
if (a.t != null && b.t != null) this.setLinePreview(a, b, '#65b7cf', 2); if (a.t != null && b.t != null) this.setLinePreview(a, b, '#65b7cf', 2);
} }
@ -1879,6 +1938,28 @@ class ConfluenceChart {
node.focusTimer = setTimeout(() => node.root.classList.remove('focus'), 800); node.focusTimer = setTimeout(() => node.root.classList.remove('focus'), 800);
} }
setFibPreview(start, end) {
if (!this.fibPreview || start?.p == null || end?.p == null || start.p === end.p) return;
if (this.previewLine) this.previewLine.setAttribute('hidden', '');
this.fibPreview.removeAttribute('hidden');
this.fibPreview.replaceChildren();
const width = this.overlayLayer?.clientWidth || this.chartEl.clientWidth;
for (const item of ConfluenceChart.FIB_RATIOS) {
const y = this.candles.priceToCoordinate(start.p + (end.p - start.p) * item.r);
if (y == null) continue;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', 0);
line.setAttribute('x2', width);
line.setAttribute('y1', y);
line.setAttribute('y2', y);
line.setAttribute('stroke', '#c8992f');
line.setAttribute('stroke-width', item.r === 0 || item.r === 0.618 || item.r === 1 ? 1.5 : 1);
if (item.r !== 0.618) line.setAttribute('stroke-dasharray', '4 4');
line.setAttribute('opacity', '0.75');
this.fibPreview.appendChild(line);
}
}
setLinePreview(start, end, color, lineWidth) { setLinePreview(start, end, color, lineWidth) {
if (start.t === end.t) return; if (start.t === end.t) return;
const x1 = this.chart.timeScale().timeToCoordinate(start.t); const x1 = this.chart.timeScale().timeToCoordinate(start.t);
@ -1898,6 +1979,10 @@ class ConfluenceChart {
clearLinePreview() { clearLinePreview() {
if (this.previewLine) this.previewLine.setAttribute('hidden', ''); if (this.previewLine) this.previewLine.setAttribute('hidden', '');
if (this.fibPreview) {
this.fibPreview.setAttribute('hidden', '');
this.fibPreview.replaceChildren();
}
} }
barAtTime(time) { barAtTime(time) {
@ -1970,6 +2055,13 @@ class ConfluenceChart {
const distance = Math.abs(lineY - param.point.y); const distance = Math.abs(lineY - param.point.y);
if (distance <= 6 && (!best || distance < best.distance)) best = { id: level.id, distance }; if (distance <= 6 && (!best || distance < best.distance)) best = { id: level.id, distance };
} }
for (const fib of this.fibs.filter(item => !item.hidden)) {
for (const level of ConfluenceChart.fibPrices(fib)) {
const y = this.candles.priceToCoordinate(level.price);
const distance = y == null ? Infinity : Math.abs(y - param.point.y);
if (distance <= 6 && (!best || distance < best.distance)) best = { id: fib.id, distance };
}
}
return best?.id || null; return best?.id || null;
} }
@ -2314,10 +2406,16 @@ class ConfluenceChart {
})); }));
} }
fibAsHandleLevel() {
const fib = this.fibs.find(item => item.id === this.selectedLineId && !item.hidden);
if (!fib) return null;
return { ...fib, kind: 'manual', slope: Number(fib.slope) || 0 };
}
renderAnchorHandles() { renderAnchorHandles() {
const level = this.levels.find( const level = this.levels.find(
value => value.id === this.selectedLineId && value.kind === 'manual' && !value.hidden, value => value.id === this.selectedLineId && value.kind === 'manual' && !value.hidden,
); ) || this.fibAsHandleLevel();
if (!level || !this.anchorHandles.length) { if (!level || !this.anchorHandles.length) {
this.anchorHandles.forEach(handle => handle.setAttribute('hidden', '')); this.anchorHandles.forEach(handle => handle.setAttribute('hidden', ''));
this.lineHitTarget?.setAttribute('hidden', ''); this.lineHitTarget?.setAttribute('hidden', '');
@ -2723,6 +2821,16 @@ class ConfluenceChart {
} }
} }
ConfluenceChart.FIB_RATIOS = [
{ r: 0, label: '0' },
{ r: 0.236, label: '23.6' },
{ r: 0.382, label: '38.2' },
{ r: 0.5, label: '50' },
{ r: 0.618, label: '61.8' },
{ r: 0.786, label: '78.6' },
{ r: 1, label: '100' },
];
ConfluenceChart.tfColors = { ConfluenceChart.tfColors = {
'1m':'#82909f', '2m':'#8a92df', '5m':'#65b7cf', '15m':'#45c39b', '1m':'#82909f', '2m':'#8a92df', '5m':'#65b7cf', '15m':'#45c39b',
'30m':'#a8c85d', '1h':'#efb643', '1d':'#d96073', '30m':'#a8c85d', '1h':'#efb643', '1d':'#d96073',

View file

@ -31,6 +31,7 @@
<span v-if="extraDetail">HELD <b>{{ status.bars_held?.[timeframe] || 0 }} {{ timeframe }}</b></span> <span v-if="extraDetail">HELD <b>{{ status.bars_held?.[timeframe] || 0 }} {{ timeframe }}</b></span>
<span v-if="armedTool === 'trendline'" class="arm-hint">Drag on the chart from one point to the other</span> <span v-if="armedTool === 'trendline'" class="arm-hint">Drag on the chart from one point to the other</span>
<span v-else-if="armedTool === 'level'" class="arm-hint">Click or drag on the chart to set the price</span> <span v-else-if="armedTool === 'level'" class="arm-hint">Click or drag on the chart to set the price</span>
<span v-else-if="armedTool === 'fibonacci'" class="arm-hint">Drag from one swing to the other</span>
<span v-else-if="selectedDrawing" class="arm-hint">Drawing selected — Delete removes it</span> <span v-else-if="selectedDrawing" class="arm-hint">Drawing selected — Delete removes it</span>
<button v-if="diagnosticMode" class="diag-capture" :disabled="captureBusy" @click="captureDiagnostic"> <button v-if="diagnosticMode" class="diag-capture" :disabled="captureBusy" @click="captureDiagnostic">
{{ captureBusy ? 'CAPTURING…' : 'CAPTURE DIAGNOSTIC' }} {{ captureBusy ? 'CAPTURING…' : 'CAPTURE DIAGNOSTIC' }}
@ -129,6 +130,20 @@
<p class="hint">Drag on the chart, or type an exact price. Alerts whenever price reaches it, whatever the confluence score.</p> <p class="hint">Drag on the chart, or type an exact price. Alerts whenever price reaches it, whatever the confluence score.</p>
</div> </div>
</div> </div>
<div class="tool" :class="{armed: armedTool === 'fibonacci'}">
<button class="tool-head" @click="armTool('fibonacci')" :aria-pressed="armedTool === 'fibonacci'">
<span class="tool-glyph"><i class="fa-solid fa-chart-line"></i></span>Fibonacci
<span class="tool-state">{{ armedTool === 'fibonacci' ? 'drag swings' : '' }}</span>
</button>
<div class="tool-body" v-show="armedTool === 'fibonacci'">
<label>Label<input v-model.trim="drawName" placeholder="optional" aria-label="Fibonacci label"></label>
<div class="row">
<label>Colour<input type="color" v-model="drawColor" aria-label="Fibonacci colour"></label>
<label>Width<select v-model.number="drawWidth" aria-label="Fibonacci width"><option v-for="width in 9" :value="width">{{ width }}px</option></select></label>
</div>
<p class="hint">Drag from one swing to the other. Levels at 0, 23.6, 38.2, 50, 61.8, 78.6 and 100.</p>
</div>
</div>
<div class="tool tool-trendline" :class="{armed: armedTool === 'trendline'}"> <div class="tool tool-trendline" :class="{armed: armedTool === 'trendline'}">
<button class="tool-head" @click="armTool('trendline')" :aria-pressed="armedTool === 'trendline'"> <button class="tool-head" @click="armTool('trendline')" :aria-pressed="armedTool === 'trendline'">
<span class="tool-glyph"><i class="fa-solid fa-arrow-trend-up"></i></span>Trendline <span class="tool-glyph"><i class="fa-solid fa-arrow-trend-up"></i></span>Trendline
@ -235,6 +250,7 @@
<option value="level">Levels</option> <option value="level">Levels</option>
<option value="comment">Comments</option> <option value="comment">Comments</option>
<option value="symbol">Symbols</option> <option value="symbol">Symbols</option>
<option value="fibonacci">Fibonacci</option>
</select> </select>
<select v-model="drawingTf" aria-label="Filter drawings by timeframe"> <select v-model="drawingTf" aria-label="Filter drawings by timeframe">
<option value="all">All TFs</option> <option value="all">All TFs</option>
@ -288,6 +304,7 @@
<div class="drawing-secondary"> <div class="drawing-secondary">
<span v-if="item.kind === 'comment'">#{{ item.number }} · NOTE · {{ item.comment.pinned ? 'PIN' : 'FLOAT' }}</span> <span v-if="item.kind === 'comment'">#{{ item.number }} · NOTE · {{ item.comment.pinned ? 'PIN' : 'FLOAT' }}</span>
<span v-else-if="item.kind === 'symbol'">#{{ item.number }} · SYMBOL · {{ item.comment.note }}</span> <span v-else-if="item.kind === 'symbol'">#{{ item.number }} · SYMBOL · {{ item.comment.note }}</span>
<span v-else-if="item.kind === 'fibonacci'">#{{ item.number }} · FIB · {{ item.label }}</span>
<span v-else-if="item.kind === 'level'" class="level-editors"> <span v-else-if="item.kind === 'level'" class="level-editors">
<label>Price<input type="number" min="0.25" step="0.25" :value="item.line.anchor_p" <label>Price<input type="number" min="0.25" step="0.25" :value="item.line.anchor_p"
aria-label="Level price in drawing list" aria-label="Level price in drawing list"

42
tests/test_fibonacci.py Normal file
View file

@ -0,0 +1,42 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.analysis.levels import Side
from app.analysis.manual_lines import ManualLine, ManualLineStore
from app.api.routes import router
from app.bars.models import Timeframe
from app.config import Settings
from app.runtime import Runtime
def test_a_fibonacci_is_not_a_confluence_level(tmp_path):
store = ManualLineStore(tmp_path / "lines.json")
store.add(ManualLine(
id="ml_fib", tf=Timeframe.M15, side=Side.RESISTANCE,
anchor_t=1000, anchor_p=6500, slope=-1 / 1800, last_t=2800,
created_at=1000, kind="fibonacci", note="swing",
))
assert store.levels() == []
assert store.drawings()[0].drawing_kind == "fibonacci"
def test_creating_a_fibonacci_returns_the_drawing_not_a_level(tmp_path):
app = FastAPI()
app.include_router(router)
app.state.runtime = Runtime(Settings(
manual_lines_path=tmp_path / "lines.json",
events_path=tmp_path / "events.json",
alert_state_path=tmp_path / "alerts.json",
chart_auth_token="",
))
client = TestClient(app)
response = client.post("/api/lines", json={
"tf": "15m", "side": "resistance",
"anchor_t": 1000, "anchor_p": 6500, "end_t": 2800, "end_p": 6400,
"kind": "fibonacci", "note": "high to low",
})
assert response.status_code == 201
body = response.json()
assert body["kind"] == "fibonacci"
assert "weight" not in body
assert app.state.runtime.manual_lines.levels() == []

View file

@ -80,9 +80,11 @@ def test_an_annotation_is_never_a_level(tmp_path):
note="watch this", **common)) note="watch this", **common))
store.add(ManualLine(id="ml_symbol", slope=0.0, kind="symbol", store.add(ManualLine(id="ml_symbol", slope=0.0, kind="symbol",
icon="skull", note="Skull", **common)) icon="skull", note="Skull", **common))
store.add(ManualLine(id="ml_fib", slope=0.01, kind="fibonacci",
note="fib", **common))
assert [level.id for level in store.levels()] == ["ml_level"] assert [level.id for level in store.levels()] == ["ml_level"]
assert [line.id for line in store.drawings()] == ["ml_level", "ml_note", "ml_symbol"] assert [line.id for line in store.drawings()] == ["ml_level", "ml_note", "ml_symbol", "ml_fib"]
def test_drawing_kind_is_derived_for_lines_saved_before_comments(tmp_path): def test_drawing_kind_is_derived_for_lines_saved_before_comments(tmp_path):