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:
|
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."""
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
112
static/chart.js
112
static/chart.js
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
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))
|
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):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue