本页源码来自 TradingView 公开发布的开源脚本,版权归原作者所有, 请遵循其原始许可(Pine 脚本常见 CC BY-NC-SA / MPL-2.0 / MIT)。 本项目仅用于研究检索与许可范围内的移植。
//@version=6
// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
// https://creativecommons.org/licenses/by-nc-sa/4.0/
// © Zeiierman {
indicator("Buyers & Sellers Profile + Dynamic S/R (Zeiierman)",overlay=true,max_lines_count=250,max_labels_count=500,max_boxes_count=300,max_bars_back=5000)
//}
// ~~ Tooltips {
var string tt1="Number of historical bars used by the large rolling volume profile. The same lookback is reconstructed historically when a pivot is evaluated, so old pivots are judged only with information available at that time."
var string tt2="Number of price rows used to discretize the rolling profile. More rows increase price resolution but also increase drawing and calculation load."
var string tt3="Maximum horizontal width of the displayed profile in chart bars. The POC row reaches this width; other rows scale relative to it."
var string tt4="Horizontal gap between the current bar and the left edge of the displayed rolling profile."
var string tt5="Master switch for the large rolling profile shown at the right side of the chart."
var string tt6="Highlight the highest-volume profile row and draw its short guide line and price label."
var string tt7="Outline profile rows that currently contain an active profile-guided support or resistance level."
var string tt8="Left/right pivot confirmation length. A value of 5 means a pivot is confirmed five bars after the actual swing bar."
var string tt9="Maximum ATR distance around a raw pivot in which the profile engine may search for a stronger nearby volume node."
var string tt10="Minimum candidate-node volume relative to the profile POC. Lower values admit weaker nearby profile structure."
var string tt11="Minimum combined profile evidence needed before a raw price pivot can become an active level."
var string tt12="Maximum fraction of the distance from the raw pivot toward the selected profile node. Actual pull is scaled by profile evidence."
var string tt13="Minimum final 0-100 score required after profile evidence, absorption, relative pivot volume and wick rejection are combined."
var string tt14="Maximum number of active profile-guided levels. When exceeded, the weakest active level is removed first."
var string tt15="Same-side levels within this ATR distance are treated as duplicates. A stronger new candidate replaces the weaker nearby level."
var string tt16="ATR buffer beyond an active level required for a confirmed close to invalidate that level."
var string tt17="Color used for active support levels, support endpoint dots and support profile-row outlines."
var string tt18="Color used for active resistance levels, resistance endpoint dots and resistance profile-row outlines."
var string tt19="Accent color used for the inner profile-confirmed pivot core and the raw-pivot-to-final-level pull guide."
var string tt20="Color of the faint raw price pivot marker before the profile engine relocates the final level."
var string tt21="Estimated buying-volume color in the large profile. Buy/sell attribution comes from candle close location inside each bar range."
var string tt22="Estimated selling-volume color in the large profile. Buy/sell attribution is approximate rather than true bid/ask volume."
var string tt23="Color used for the displayed profile point of control."
var string tt24="Shared line style used by active support and resistance levels."
var string tt25="Show the layered profile-confirmed pivot marker at the final relocated level price."
var string tt26="Show the faint raw pivot marker and dotted vertical guide connecting it to the final profile-guided level."
var string tt27="Show the final 0-100 level score inside the historical profile-confirmed pivot marker."
var string tt28="Show the current endpoint dot and one-number score label at the right edge of each active level."
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Groups {
gPROF="Large Rolling Profile"
gENG="Profile-Guided Pivot Engine"
gSTY="Visual Style"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Inputs {
profLb=input.int(600,"Lookback",minval=100,maxval=1500,group=gPROF,inline="p1")
profRows=input.int(72,"Rows",minval=24,maxval=100,group=gPROF,inline="p1",tooltip=tt1+"\n\n"+tt2)
profW=input.int(58,"Width",minval=15,maxval=110,group=gPROF,inline="p2")
profOff=input.int(8,"Offset",minval=2,maxval=50,group=gPROF,inline="p2",tooltip=tt3+"\n\n"+tt4)
showProf=input.bool(true,"Show Profile",group=gPROF,inline="p3")
showPoc=input.bool(true,"POC",group=gPROF,inline="p3",active=showProf)
showLvlRows=input.bool(true,"Level Rows",group=gPROF,inline="p3",active=showProf,tooltip=tt5+"\n\n"+tt6+"\n\n"+tt7)
swingLen=input.int(5,"Swing Length",minval=2,maxval=25,group=gENG,inline="e1")
srchAtr=input.float(1.20,"Search ATR",minval=0.20,maxval=3.00,step=0.05,group=gENG,inline="e1",tooltip=tt8+"\n\n"+tt9)
nodeMin=input.float(0.08,"Min Node",minval=0.0,maxval=1.0,step=0.01,group=gENG,inline="e2")
evidMin=input.float(0.22,"Min Evidence",minval=0.0,maxval=1.0,step=0.01,group=gENG,inline="e2",tooltip=tt10+"\n\n"+tt11)
pullMax=input.float(0.85,"Profile Pull",minval=0.0,maxval=1.0,step=0.05,group=gENG,inline="e3")
scoreMin=input.float(30.0,"Min Score",minval=0,maxval=100,step=1,group=gENG,inline="e3",tooltip=tt12+"\n\n"+tt13)
maxLvl=input.int(20,"Max Levels",minval=2,maxval=40,group=gENG,inline="e4")
mergeAtr=input.float(0.16,"Merge ATR",minval=0.0,maxval=1.0,step=0.02,group=gENG,inline="e4")
breakAtr=input.float(0.10,"Break ATR",minval=0.0,maxval=0.5,step=0.01,group=gENG,inline="e4",tooltip=tt14+"\n\n"+tt15+"\n\n"+tt16)
supCol=input.color(color.rgb(31,199,116),"Support",group=gSTY,inline="c1")
resCol=input.color(color.rgb(239,75,75),"Resistance",group=gSTY,inline="c1",tooltip=tt17+"\n\n"+tt18)
coreCol=input.color(color.rgb(255,205,64),"Pivot Core",group=gSTY,inline="c2")
rawCol=input.color(color.rgb(160,166,178),"Raw Pivot",group=gSTY,inline="c2",tooltip=tt19+"\n\n"+tt20)
buyCol=input.color(color.rgb(43,158,143),"Profile Buy",group=gSTY,inline="c3")
sellCol=input.color(color.rgb(74,88,182),"Profile Sell",group=gSTY,inline="c3")
pocCol=input.color(color.rgb(255,145,32),"POC",group=gSTY,inline="c3",tooltip=tt21+"\n\n"+tt22+"\n\n"+tt23)
lvlStyle=input.string("Solid","Line Style",options=["Solid","Dotted","Dashed"],group=gSTY,inline="s1",tooltip=tt24)
showDots=input.bool(true,"Pivot Dots",group=gSTY,inline="s2")
showRaw=input.bool(true,"Raw Pivot",group=gSTY,inline="s2",active=showDots)
showDotScore=input.bool(true,"Dot Score",group=gSTY,inline="s2",active=showDots)
showEdge=input.bool(true,"Right Score",group=gSTY,inline="s2",tooltip=tt25+"\n\n"+tt26+"\n\n"+tt27+"\n\n"+tt28)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Constants {
int VOL_LEN=40
int ABS_RAD=math.min(3,swingLen)
float PIV_VOL_X=2.2
float ABS_BODY_MAX=0.40
float ABS_VOL_X=1.5
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Types {
type Lvl
float p
float sc
float raw
float node
float evid
float nStr
bool supp
int born
line ln
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Helpers {
clip(float x)=>math.max(0.0,math.min(1.0,x))
lstyle(string s)=>s=="Dotted"?line.style_dotted:s=="Dashed"?line.style_dashed:line.style_solid
lwidth(float s)=>s>=82?4:s>=62?3:2
lalpha(float s)=>s>=82?8:s>=62?22:s>=45?38:52
rings(float s)=>s>=82?4:s>=66?3:s>=50?2:1
rsize(int r)=>r>=4?size.large:r==3?size.normal:r==2?size.small:size.tiny
ralpha(int r)=>r>=4?68:r==3?52:r==2?30:4
row_id(float p,float bot,float step,int rows)=>math.max(0,math.min(rows-1,int(math.floor((p-bot)/step))))
lvl_col(bool supp)=>supp?supCol:resCol
clear_boxes(array<box> a)=>
while a.size()>0
box.delete(a.pop())
clear_lines(array<line> a)=>
while a.size()>0
line.delete(a.pop())
clear_labels(array<label> a)=>
while a.size()>0
label.delete(a.pop())
profile(int newest,int oldest,int rows)=>
float lo=low[newest]
float hi=high[newest]
for i=newest to oldest
lo:=math.min(lo,low[i])
hi:=math.max(hi,high[i])
float span=math.max(hi-lo,syminfo.mintick*rows)
float bot=lo-(span-(hi-lo))*0.5
float step=span/rows
array<float> tot=array.new_float(rows,0.0)
array<float> buy=array.new_float(rows,0.0)
for i=newest to oldest
float l=low[i]
float h=high[i]
float r=h-l
float v=math.max(nz(volume[i],0.0),0.0)
float bf=r>0?clip((close[i]-l)/r):0.5
int r0=row_id(l,bot,step,rows)
int r1=row_id(h,bot,step,rows)
for j=r0 to r1
float rl=bot+j*step
float ov=r>0?math.max(0.0,math.min(h,rl+step)-math.max(l,rl)):step
float frac=r>0?ov/r:1.0
if frac>0
float av=v*frac
tot.set(j,tot.get(j)+av)
buy.set(j,buy.get(j)+av*bf)
[bot,step,tot,buy]
pivot_features(bool supp,float avgVol)=>
float vb=nz(avgVol[swingLen],volume[swingLen])
float vr=vb>0?nz(volume[swingLen],0.0)/vb:1.0
float vSc=clip((vr-0.8)/(PIV_VOL_X-0.8))
float pr=math.max(high[swingLen]-low[swingLen],syminfo.mintick)
float wick=supp?math.min(open[swingLen],close[swingLen])-low[swingLen]:high[swingLen]-math.max(open[swingLen],close[swingLen])
float wSc=clip((wick/pr)/0.50)
float absSc=0.0
for n=0 to ABS_RAD*2
int o=swingLen-ABS_RAD+n
float cr=math.max(high[o]-low[o],syminfo.mintick)
float v=math.max(nz(volume[o],0.0),0.0)
float base=nz(avgVol[o],v)
float vRat=base>0?v/base:1.0
float bRat=math.abs(close[o]-open[o])/cr
float small=clip((ABS_BODY_MAX-bRat)/ABS_BODY_MAX)
float av=clip((vRat-1.0)/(ABS_VOL_X-1.0))
absSc:=math.max(absSc,math.sqrt(small*av))
[vSc,wSc,absSc]
profile_node(float raw,bool supp,float atrP,float bot,float step,array<float> tot,array<float> buy)=>
float mx=math.max(tot.max(),0.0000001)
float rad=math.max(atrP*srchAtr,step*2.0)
float bestP=raw
float bestE=-1.0
float bestS=0.0
float bestPk=0.0
float bestDir=0.5
int rows=tot.size()
for r=0 to rows-1
float p=bot+(r+0.5)*step
float d=math.abs(p-raw)
if d<=rad
float rv=tot.get(r)
float str=rv/mx
float lv=r>0?tot.get(r-1):rv
float rv2=r<rows-1?tot.get(r+1):rv
float nAvg=math.max((lv+rv2)*0.5,mx*0.01)
float pk=clip((rv/nAvg-0.85)/0.65)
float bSh=rv>0?clip(buy.get(r)/rv):0.5
float dir=supp?bSh:1.0-bSh
float prox=1.0-d/rad
float e=0.50*str+0.20*pk+0.20*dir+0.10*prox
if e>bestE
bestE:=e
bestP:=p
bestS:=str
bestPk:=pk
bestDir:=dir
[bestP,math.max(bestE,0.0),bestS,bestPk,bestDir]
nearby(array<Lvl> a,float p,bool supp,float tol)=>
int id=-1
float best=1e20
if a.size()>0
for i=0 to a.size()-1
Lvl lv=a.get(i)
if lv.supp==supp
float d=math.abs(lv.p-p)
if d<=tol and d<best
best:=d
id:=i
id
make_line(int born,float p,bool supp,float sc)=>line.new(born,p,time,p,xloc=xloc.bar_time,extend=extend.none,
color=color.new(lvl_col(supp),lalpha(sc)),width=lwidth(sc),style=lstyle(lvlStyle))
store_lvl(array<Lvl> a,float p,float raw,float node,float evid,float nStr,bool supp,float sc,int born,float atrP)=>
bool changed=false
float tol=math.max(atrP*mergeAtr,syminfo.mintick)
int id=nearby(a,p,supp,tol)
if id>=0
Lvl lv=a.get(id)
if sc>lv.sc
lv.p:=p
lv.sc:=sc
lv.raw:=raw
lv.node:=node
lv.evid:=evid
lv.nStr:=nStr
lv.born:=born
line.set_xy1(lv.ln,born,p)
line.set_xy2(lv.ln,time,p)
line.set_color(lv.ln,color.new(lvl_col(supp),lalpha(sc)))
line.set_width(lv.ln,lwidth(sc))
a.set(id,lv)
changed:=true
else
a.push(Lvl.new(p,sc,raw,node,evid,nStr,supp,born,make_line(born,p,supp,sc)))
changed:=true
changed
remove_lvl(array<Lvl> a,int id)=>
Lvl lv=a.get(id)
line.delete(lv.ln)
a.remove(id)
0
trim_lvls(array<Lvl> a)=>
while a.size()>maxLvl
int weak=0
float sc=a.get(0).sc
for i=1 to a.size()-1
if a.get(i).sc<sc
sc:=a.get(i).sc
weak:=i
remove_lvl(a,weak)
0
update_lvls(array<Lvl> a)=>
if a.size()>0
for i=0 to a.size()-1
Lvl lv=a.get(i)
line.set_xy2(lv.ln,time,lv.p)
line.set_color(lv.ln,color.new(lvl_col(lv.supp),lalpha(lv.sc)))
line.set_width(lv.ln,lwidth(lv.sc))
line.set_style(lv.ln,lstyle(lvlStyle))
0
level_row(array<Lvl> a,float lo,float hi)=>
int side=0
if a.size()>0
for i=0 to a.size()-1
Lvl lv=a.get(i)
if lv.p>=lo and lv.p<=hi
side:=lv.supp?1:-1
break
side
score_tip(Lvl lv)=>"Raw pivot: "+str.tostring(lv.raw,format.mintick)+"\nProfile node: "+str.tostring(lv.node,format.mintick)
+"\nProfile evidence: "+str.tostring(lv.evid*100.0,"#")+"%\nNode strength vs POC: "+str.tostring(lv.nStr*100.0,"#")
+"%\nFinal level: "+str.tostring(lv.p,format.mintick)
pivot_eval(array<Lvl> a,float raw,bool supp,float avgVol,float atr)=>
int avail=bar_index-swingLen+1
int bars=math.max(1,math.min(profLb,avail))
int newest=swingLen
int oldest=swingLen+bars-1
[bot,step,tot,buy]=profile(newest,oldest,profRows)
float atrP=math.max(nz(atr[swingLen],syminfo.mintick),syminfo.mintick)
[node,evid,nStr,pk,dir]=profile_node(raw,supp,atrP,bot,step,tot,buy)
[vSc,wSc,aSc]=pivot_features(supp,avgVol)
bool ok=nStr>=nodeMin and evid>=evidMin
float pull=ok?pullMax*clip(evid/0.65):0.0
float p=raw+(node-raw)*pull
float sc=100.0*(0.50*evid+0.20*aSc+0.15*vSc+0.15*wSc)
bool stored=ok and sc>=scoreMin?store_lvl(a,p,raw,node,evid,nStr,supp,sc,time[swingLen],atrP):false
[stored,sc,p,node,evid,nStr,pk,dir]
manage_breaks(array<Lvl> a,float atr)=>
bool sBreak=false
bool rBreak=false
if a.size()>0
for i=a.size()-1 to 0
Lvl lv=a.get(i)
float buf=math.max(nz(atr,0.0)*breakAtr,0.0)
bool broken=barstate.isconfirmed and (lv.supp?close<lv.p-buf:close>lv.p+buf)
if broken
sBreak:=sBreak or lv.supp
rBreak:=rBreak or not lv.supp
remove_lvl(a,i)
[sBreak,rBreak]
draw_markers(array<Lvl> a,array<label> lbs,array<line> lns)=>
if showDots and a.size()>0
for i=0 to a.size()-1
Lvl lv=a.get(i)
color base=lvl_col(lv.supp)
if showRaw
lbs.push(label.new(x=lv.born,y=lv.raw,text="",xloc=xloc.bar_time,yloc=yloc.price,style=label.style_circle,
color=color.new(rawCol,60),textcolor=rawCol,size=size.tiny))
lns.push(line.new(x1=lv.born,y1=lv.raw,x2=lv.born,y2=lv.p,xloc=xloc.bar_time,color=color.new(coreCol,45),
width=1,style=line.style_dotted))
int rc=rings(lv.sc)
for r=rc to 1
color fill=r==1?color.new(coreCol,lv.evid>=0.45?0:22):color.new(base,ralpha(r))
lbs.push(label.new(x=lv.born,y=lv.p,text="",xloc=xloc.bar_time,yloc=yloc.price,style=label.style_circle,
color=fill,textcolor=color.white,size=rsize(r)))
if showDotScore
lbs.push(label.new(x=lv.born,y=lv.p,text=str.tostring(lv.sc,"#"),xloc=xloc.bar_time,yloc=yloc.price,
style=label.style_none,color=color.new(color.black,100),textcolor=color.white,size=size.tiny,
tooltip=score_tip(lv)))
0
draw_edge(array<Lvl> a,array<label> lbs)=>
if showEdge and a.size()>0
for i=0 to a.size()-1
Lvl lv=a.get(i)
color c=lvl_col(lv.supp)
lbs.push(label.new(x=bar_index,y=lv.p,text="",xloc=xloc.bar_index,yloc=yloc.price,style=label.style_circle,
color=c,textcolor=c,size=size.tiny))
lbs.push(label.new(x=bar_index+1,y=lv.p,text=str.tostring(lv.sc,"#"),xloc=xloc.bar_index,yloc=yloc.price,
style=label.style_label_left,color=color.new(c,100),textcolor=c,size=size.small))
0
draw_profile(array<Lvl> a,array<box> bx,array<line> ln,array<label> lb)=>
int bars=math.max(1,math.min(profLb,bar_index+1))
[bot,step,tot,buy]=profile(0,bars-1,profRows)
float mx=tot.max()
if mx>0
int poc=tot.indexof(mx)
int x=bar_index+profOff
for r=0 to profRows-1
float rv=tot.get(r)
if rv>0
float str=rv/mx
float bSh=clip(buy.get(r)/rv)
int tw=math.max(1,int(math.round(profW*str)))
int bw=int(math.round(tw*bSh))
int sw=tw-bw
float y0=bot+(r+0.08)*step
float y1=bot+(r+0.92)*step
int fade=int(math.round(76.0-46.0*str))
if bw>0
bx.push(box.new(left=x,top=y1,right=x+bw,bottom=y0,xloc=xloc.bar_index,border_color=color.new(buyCol,100),
bgcolor=color.new(buyCol,fade)))
if sw>0
bx.push(box.new(left=x+bw,top=y1,right=x+tw,bottom=y0,xloc=xloc.bar_index,border_color=color.new(sellCol,100),
bgcolor=color.new(sellCol,fade)))
if showPoc and r==poc
bx.push(box.new(left=x,top=y1,right=x+tw,bottom=y0,xloc=xloc.bar_index,border_color=pocCol,border_width=2,
bgcolor=color.new(pocCol,88)))
if showLvlRows
int side=level_row(a,y0,y1)
if side!=0
color c=side>0?supCol:resCol
bx.push(box.new(left=x,top=y1,right=x+tw,bottom=y0,xloc=xloc.bar_index,border_color=c,border_width=2,
bgcolor=color.new(coreCol,93)))
if showPoc
float p=bot+(poc+0.5)*step
ln.push(line.new(x1=math.max(0,bar_index-18),y1=p,x2=x,y2=p,xloc=xloc.bar_index,color=color.new(pocCol,25),width=2))
lb.push(label.new(x=x+profW+2,y=p,text="POC "+str.tostring(p,format.mintick),xloc=xloc.bar_index,yloc=yloc.price,
style=label.style_label_left,color=color.new(pocCol,100),textcolor=pocCol,size=size.tiny))
0
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Core Series {
avgVol=ta.sma(volume,VOL_LEN)
atr=ta.atr(14)
ph=ta.pivothigh(high,swingLen,swingLen)
pl=ta.pivotlow(low,swingLen,swingLen)
ready=bar_index>VOL_LEN+swingLen*2+5
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Objects & State {
var array<Lvl> lvls=array.new<Lvl>()
var array<label> mLb=array.new_label()
var array<label> eLb=array.new_label()
var array<label> pLb=array.new_label()
var array<box> pBx=array.new_box()
var array<line> tLn=array.new_line()
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Pivot Evaluation {
bool newSup=false
bool newRes=false
if ready and not na(pl)
[stored,sc,p,node,evid,nStr,pk,dir]=pivot_eval(lvls,pl,true,avgVol,atr)
newSup:=stored
if ready and not na(ph)
[stored,sc,p,node,evid,nStr,pk,dir]=pivot_eval(lvls,ph,false,avgVol,atr)
newRes:=stored
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Level Lifecycle {
[supBreak,resBreak]=manage_breaks(lvls,atr)
trim_lvls(lvls)
update_lvls(lvls)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Drawing {
if barstate.islast
clear_labels(mLb)
clear_labels(eLb)
clear_labels(pLb)
clear_boxes(pBx)
clear_lines(tLn)
draw_markers(lvls,mLb,tLn)
draw_edge(lvls,eLb)
if showProf
draw_profile(lvls,pBx,tLn,pLb)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Alerts {
alertcondition(newSup,"New profile-confirmed support","A structural pivot low was confirmed by the large rolling volume profile and converted into support.")
alertcondition(newRes,"New profile-confirmed resistance","A structural pivot high was confirmed by the large rolling volume profile and converted into resistance.")
alertcondition(supBreak,"Support broken","An active profile-guided support was broken on a confirmed close.")
alertcondition(resBreak,"Resistance broken","An active profile-guided resistance was broken on a confirmed close.")
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}