From a slightly less "dsp-like" point of view, slightly more geometric / time series, but this also works:
The relation between the sinusoid (of amplitude 1) and the unit circle is well known.
Instead of thinking of a moving average as a geometric mean on a window that slides from left to right over the time series, you could also define it as the cumulative sum of a lower ($*1/n$) amplitude sine (from right to left in the window), as the window slides from left to right over the time series: I refer to the left panel in the image below:
(best to open the image in a new tab)

Now looking at the right panel in the image above, at the unit circle:
the average can be seen as the cumulative sum of the vectors formed between
the circle center and the purple dots on the small circle: the x- and y-values are respectively the cos & sin of $90° + 0:(n-1) * 360/period * 1/n$). This cumulative vector sum also lies on a circle: the dotted cyan circle's centre lies at $(0.6353,0.05002)$, and has a radius of $0.63726$. The horizontal coordinate of the circle center lies at +/- $1/(2*n)$.
You then calculate the length from the end point of the circular segment to the origin, using the sum of the x-values and y-values via pythagoras' theorem, and you get your amplitude reduction, as indicated by the length of the radius of the dotted red circle, in this case $0.9012426$. The circle will rotate if you chose a different start point (not $90°$), but the end of the circular segment will always be on the same dotted red circle.
DegreesPerSample<-360/p
sumxvalues<-sum(cos(((90+(0:(n-1))*DegreesPerSample))*pi/180)/n)
sumyvalues<-sum(sin(((90+(0:(n-1))*DegreesPerSample))*pi/180)/n)
((sumxvalues^2)+(sumyvalues^2))^.5
I believe there are parallels with the Hilbert Transform's In-Phase and Quadrature components?
The lag of a simple moving average is $(n-1)/2$ (I believe you dsp guys call this group delay). This is also the center of gravity of the rectangular impulse response. You can also read that off the circular phase delay plot: for $p=40$ and $n=10$, the cyan arrow shows that the angle of the end of the circular segment to the origin is $130.5$ degrees: $90$ (the startpoint)$ + (n-1)/2 * 360/p$. It can also be seen as $4.5$: $(n-1)/2$ small purple circle segments on the unit circle.
But there is more information to be found here:
there are causal filters with negative weights at the back (left side) of the window. Using negative weights, you can create a causal filter that is "in-phase" with a sinusoidal signal. In the $n=10, p=40$ case, instead of the (SMA) weights:
0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
if you use:
-0.1000 -0.1000 -0.1000 0.0764 0.1000 0.1000 0.1000 0.1000 0.1000 0.1000
which is the equivalent of $31.17959$ % negative weights, the adapted simple moving average filter's result will be "in-phase" with the sinusoid.
If you use the same amplitude reduction concept, and you multiply this new filter's output's amplitude with $2.051051$, you get your original sinusoid, constructed from a causal filter on a window of length $n$. A quick calculation learns that the sum of the weights of the new amplitude-adjusted weights is $0.7720322$.
All good and well when you know the period of your sinusoid. But how could you create the so-called xvalues (cosines of the angles) from the yvalues (values of "a" signal in the window $/n$)? For an arbitrary -unknown- period and amplitude?
Updated 10/04/20:
So, how do you calculate the % of negative weights at the left side of the window in the convolution such that the filter's result is in-phase with a sinusoid of period p? You have 2 percentages: the negative (p1), and the other one, so I called it "p1function":
p1function<-function(n,DegreesPerSample)
{
xvalues3<-round(cos((90+((0:(n-1))*DegreesPerSample))*pi/180)*smavector(n),5)
yvalues3<-round(sin((90+((0:(n-1))*DegreesPerSample))*pi/180)*smavector(n),5)
outputc<-circleFromThreePoints(cumsum(xvalues3)[1],cumsum(xvalues3)[2],cumsum(xvalues3)[3],cumsum(yvalues3)[1],cumsum(yvalues3)[2],cumsum(yvalues3)[3])
hc<-outputc[1];kc<-outputc[2];rc<-outputc[3];
xc<-seq((hc-rc),(hc+rc),length.out=1001)
x1<-hc
y1<-kc
x2<-(sum(xvalues3)/2)
y2<-kc+(((rc^2)-(((sum(xvalues3)/2)-hc)^2))^0.5)
x3<-0
y3<-sinn(90+DegreesPerSample)/n
x4<-sum(xvalues3)
y4<-sum(yvalues3)
startangle<-atan2((y3-y1),(x3-x1))*180/pi
midangle<-atan2((y2-y1),(x2-x1))*180/pi
endangle<-atan2((y4-y1),(x4-x1))*180/pi
endangle<-ifelse(endangle<0,endangle+360,endangle)
p1est<-100*((endangle+startangle)-(midangle+startangle))/(endangle+startangle)
return(p1est)
}
Which uses another function to estimate a circle from 3 points:
circleFromThreePoints<-function(x1,x2,x3,y1,y2,y3)
{
vara<-x1*(y2-y3)-y1*(x2-x3)+x2*y3-x3*y2;
varb<-(x1*x1+y1*y1)*(y3-y2)+(x2*x2+y2*y2)*(y1-y3)+(x3*x3+y3*y3)*(y2-y1);
varc<-(x1*x1+y1*y1)*(x2-x3)+(x2*x2+y2*y2)*(x3-x1)+(x3*x3+y3*y3)*(x1-x2);
vard<-(x1*x1+y1*y1)*(x3*y2-x2*y3)+(x2*x2+y2*y2)*(x1*y3-x3*y1)+(x3*x3+y3*y3)*(x2*y1-x1*y2)
varx<- -varb/(2*vara)
vary<- -varc/(2*vara)
varr<- (((varb*varb)+(varc*varc)-(4*vara*vard))/(4*vara*vara))^0.5
# x, y , r:
# (x-x1)^2+(y-y1)^2 = r^2
# h,k,r for equation: (x-h)^2+(y-k)^2 = r^2
# To plot: upp<-(((r^2)-((x-h)^2))^0.5)+k & dwn<--(((r^2)-((x-h)^2))^0.5)+k
return(c(round(varx,5),round(vary,5),round(varr,5)))
}
The hashtag means it's a comment.
And of course this filter's output's amplitude is not the same, so, going back to the plot I made above,
you just need to adjust its height: same way as above:
ampfactor<-function(n,p1est,DegreesPerSample)
{
xvalues3<-cos((90+((0:(n-1))*DegreesPerSample))*pi/180)/n
yvalues3<-sin((90+((0:(n-1))*DegreesPerSample))*pi/180)/n
1/(sum(xvalues3*onesfunc(n,p1est))^2+sum(yvalues3*onesfunc(n,p1est))^2)^0.5
}
The fun thing is, I think somewhere in here there's a causal version of my favorite Hodrick-Prescott Filter.
Could lead to an accurate instantaneous frequency estimator, as it only needs 3 points -given the circle-, no?
As one varies the p1%, you get closer to the wave. This works well in theory. I do believe using negative weights at the back of the window can bring you just 1 more step closer. Last step would be using this concept on random data. Obviously causality cannot be broken, but you can get closer. Please do correct me where needed.