fib levels
This commit is contained in:
parent
08cff3ce53
commit
58a64c1eeb
8 changed files with 240 additions and 11 deletions
|
|
@ -54,6 +54,10 @@ class ManualLine:
|
|||
def is_comment(self) -> bool:
|
||||
return self.drawing_kind in {"comment", "symbol"}
|
||||
|
||||
@property
|
||||
def is_overlay(self) -> bool:
|
||||
return self.drawing_kind in {"comment", "symbol", "fibonacci"}
|
||||
|
||||
@property
|
||||
def horizontal(self) -> bool:
|
||||
"""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
|
||||
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]:
|
||||
"""Everything drawn, comments included, newest number last."""
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ class LineCreate(BaseModel):
|
|||
line_width: int = Field(2, ge=1, le=9)
|
||||
cutoff_t: int | None = None
|
||||
armed: bool = True
|
||||
kind: str = ""
|
||||
|
||||
|
||||
class PriceAlertCreate(BaseModel):
|
||||
|
|
@ -214,16 +215,17 @@ def put_ma_alerts(request: Request, payload: dict):
|
|||
|
||||
@router.post("/lines", status_code=201)
|
||||
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")
|
||||
last_t = payload.end_t if payload.end_t != payload.anchor_t else payload.anchor_t + 1
|
||||
line = ManualLine(
|
||||
id=f"ml_{uuid.uuid4().hex}",
|
||||
tf=payload.tf,
|
||||
side=payload.side,
|
||||
anchor_t=payload.anchor_t,
|
||||
anchor_p=payload.anchor_p,
|
||||
slope=(payload.end_p - payload.anchor_p) / (payload.end_t - payload.anchor_t),
|
||||
last_t=payload.end_t,
|
||||
slope=(payload.end_p - payload.anchor_p) / (last_t - payload.anchor_t),
|
||||
last_t=last_t,
|
||||
created_at=int(time.time()),
|
||||
note=payload.note,
|
||||
hidden=payload.hidden,
|
||||
|
|
@ -231,10 +233,13 @@ def create_line(request: Request, payload: LineCreate):
|
|||
line_width=payload.line_width,
|
||||
cutoff_t=payload.cutoff_t,
|
||||
armed=payload.armed,
|
||||
kind=payload.kind,
|
||||
)
|
||||
runtime = request.app.state.runtime
|
||||
line = runtime.manual_lines.add(line)
|
||||
runtime.rebuild_levels()
|
||||
if line.is_overlay:
|
||||
return {**line.to_dict(), "kind": line.drawing_kind}
|
||||
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()
|
||||
# A comment has no level form — returning one would hand the caller a shape
|
||||
# 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,
|
||||
).to_dict()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
(`indexAt` / `timeAtIndex` / `coordinateAtTime`). A mark in the right-hand
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ createApp({
|
|||
// Comments are not levels, so they never arrive over the levels socket and
|
||||
// are fetched and refreshed explicitly.
|
||||
const comments = ref([]);
|
||||
const fibs = ref([]);
|
||||
const drawingFilter = ref('');
|
||||
const drawingKind = ref('all');
|
||||
const maAlerts = ref({ '1d': [] });
|
||||
|
|
@ -281,7 +282,15 @@ createApp({
|
|||
hidden: comment.hidden,
|
||||
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(() => {
|
||||
|
|
@ -691,6 +700,10 @@ createApp({
|
|||
await createSymbol(result);
|
||||
return;
|
||||
}
|
||||
if (result.tool === 'fibonacci') {
|
||||
await createFibonacci(result.start, result.end);
|
||||
return;
|
||||
}
|
||||
if (result.tool === 'level') {
|
||||
alertPrice.value = result.price;
|
||||
await addPriceAlert();
|
||||
|
|
@ -704,7 +717,10 @@ createApp({
|
|||
if (!response.ok) return;
|
||||
const payload = await response.json();
|
||||
comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind));
|
||||
fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci');
|
||||
syncComments();
|
||||
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
|
||||
? fibs.value.filter(item => !item.hidden) : []);
|
||||
}
|
||||
|
||||
function syncComments() {
|
||||
|
|
@ -808,6 +824,27 @@ createApp({
|
|||
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) {
|
||||
const [start, end] = [first, second].sort((a, b) => a.t - b.t);
|
||||
const side = start.snappedSide || end.snappedSide || drawSide.value;
|
||||
|
|
@ -980,7 +1017,7 @@ createApp({
|
|||
const saved = await response.json();
|
||||
// A comment is not a level and must not be pushed into the level list,
|
||||
// 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);
|
||||
syncVisibleLevels();
|
||||
}
|
||||
|
|
@ -1002,6 +1039,7 @@ createApp({
|
|||
});
|
||||
if (!response.ok) { console.error(`Unable to move line: HTTP ${response.status}`); return; }
|
||||
const saved = await response.json();
|
||||
if (line.kind === 'fibonacci') { await refreshComments(); return; }
|
||||
levels.value = levels.value.map(level => level.id === line.id ? saved : level);
|
||||
syncVisibleLevels();
|
||||
}
|
||||
|
|
@ -1197,6 +1235,8 @@ createApp({
|
|||
syncVisibleLevels();
|
||||
syncComments();
|
||||
if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth);
|
||||
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
|
||||
? fibs.value.filter(item => !item.hidden) : []);
|
||||
sendPrefs();
|
||||
}, { deep: true });
|
||||
|
||||
|
|
@ -1233,7 +1273,9 @@ createApp({
|
|||
watch(selectedDrawing, id => {
|
||||
if (chartApi) {
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
112
static/chart.js
112
static/chart.js
|
|
@ -134,6 +134,9 @@ class ConfluenceChart {
|
|||
this.overlayLayer = null;
|
||||
this.rthPrimitive = null;
|
||||
this.rthEnabled = true;
|
||||
this.fibs = [];
|
||||
this.fibLines = new Map();
|
||||
this.fibPreview = null;
|
||||
this.futureSpace = null;
|
||||
this.snapDot = null;
|
||||
this.snapLabel = null;
|
||||
|
|
@ -373,6 +376,9 @@ class ConfluenceChart {
|
|||
this.previewLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
this.previewLine.setAttribute('hidden', '');
|
||||
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.lineBridgeLayer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
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) {
|
||||
this.comments = comments;
|
||||
this.scheduleOverlays(true);
|
||||
|
|
@ -1602,6 +1651,10 @@ class ConfluenceChart {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -1699,7 +1752,9 @@ class ConfluenceChart {
|
|||
const a = dragged ? this.snapPoint(start) : this.pendingAnchor;
|
||||
this.pendingAnchor = null;
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
@ -1800,6 +1855,10 @@ class ConfluenceChart {
|
|||
}
|
||||
const a = this.snapPoint(start);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -1879,6 +1938,28 @@ class ConfluenceChart {
|
|||
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) {
|
||||
if (start.t === end.t) return;
|
||||
const x1 = this.chart.timeScale().timeToCoordinate(start.t);
|
||||
|
|
@ -1898,6 +1979,10 @@ class ConfluenceChart {
|
|||
|
||||
clearLinePreview() {
|
||||
if (this.previewLine) this.previewLine.setAttribute('hidden', '');
|
||||
if (this.fibPreview) {
|
||||
this.fibPreview.setAttribute('hidden', '');
|
||||
this.fibPreview.replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
barAtTime(time) {
|
||||
|
|
@ -1970,6 +2055,13 @@ class ConfluenceChart {
|
|||
const distance = Math.abs(lineY - param.point.y);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -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() {
|
||||
const level = this.levels.find(
|
||||
value => value.id === this.selectedLineId && value.kind === 'manual' && !value.hidden,
|
||||
);
|
||||
) || this.fibAsHandleLevel();
|
||||
if (!level || !this.anchorHandles.length) {
|
||||
this.anchorHandles.forEach(handle => handle.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 = {
|
||||
'1m':'#82909f', '2m':'#8a92df', '5m':'#65b7cf', '15m':'#45c39b',
|
||||
'30m':'#a8c85d', '1h':'#efb643', '1d':'#d96073',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
<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-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>
|
||||
<button v-if="diagnosticMode" class="diag-capture" :disabled="captureBusy" @click="captureDiagnostic">
|
||||
{{ 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>
|
||||
</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'}">
|
||||
<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
|
||||
|
|
@ -235,6 +250,7 @@
|
|||
<option value="level">Levels</option>
|
||||
<option value="comment">Comments</option>
|
||||
<option value="symbol">Symbols</option>
|
||||
<option value="fibonacci">Fibonacci</option>
|
||||
</select>
|
||||
<select v-model="drawingTf" aria-label="Filter drawings by timeframe">
|
||||
<option value="all">All TFs</option>
|
||||
|
|
@ -288,6 +304,7 @@
|
|||
<div class="drawing-secondary">
|
||||
<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 === 'fibonacci'">#{{ item.number }} · FIB · {{ item.label }}</span>
|
||||
<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"
|
||||
aria-label="Level price in drawing list"
|
||||
|
|
|
|||
42
tests/test_fibonacci.py
Normal file
42
tests/test_fibonacci.py
Normal 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() == []
|
||||
|
|
@ -80,9 +80,11 @@ def test_an_annotation_is_never_a_level(tmp_path):
|
|||
note="watch this", **common))
|
||||
store.add(ManualLine(id="ml_symbol", slope=0.0, kind="symbol",
|
||||
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 [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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue